spiegel-keyman/developer/src/common/web/utils/src/compiler-interfaces.ts

531 lines
17 KiB
TypeScript

import { CompilerCallbacks } from "./compiler-callbacks.js";
import { ObjectWithCompileContext } from '@keymanapp/common-types';
import { KeymanXMLReader, XML_FILENAME_SYMBOL } from "./xml-utils.js";
/**
* Abstract interface for compiler error and warning messages
*/
export interface CompilerEvent {
filename?: string;
/** line where a message applies */
line?: number;
/**
* column where a message applies.
*/
column?: number;
/**
* offset where a message applies.
* If set, encompasses line and column.
*/
offset?: number;
code: number;
message: string;
/**
* detailed Markdown-formatted description of the error including
* references to documentation, remediation options.
*/
detail?: string;
/**
* an internal error occurred that should be captured with a stack trace
* e.g. to the Keyman sentry instance by kmc
*/
exceptionVar?: any;
};
export enum CompilerErrorSeverity {
Debug = 0x000000, // log everything including internal debug
Verbose = 0x100000, // log everything, except debug
Info = 0x200000, // Informational, not necessarily a problem
Hint = 0x300000, // Something the user might want to be aware of
Warn = 0x400000, // Warning: Not great, but we can keep going.
Error = 0x500000, // Severe error where we can't continue
Fatal = 0x600000, // OOM or should-not-happen internal problem
};
export const CompilerErrorSeverityValues = [
CompilerErrorSeverity.Debug,
CompilerErrorSeverity.Verbose,
CompilerErrorSeverity.Info,
CompilerErrorSeverity.Hint,
CompilerErrorSeverity.Warn,
CompilerErrorSeverity.Error,
CompilerErrorSeverity.Fatal,
]
/**
* Mask values for mapping compiler errors
*/
export enum CompilerErrorMask {
Severity = 0x00F00000, // includes reserved bits, 16 possible severity levels
Error = 0x000FFFFF, // error | namespace
Namespace = 0x000FF000, // 256 possible namespaces
BaseError = 0x00000FFF, // error code, 2,048 possible error codes per namespace
Reserved = 0xFF000000, // do not use these error values at this time
};
const errorSeverityName = {
[CompilerErrorSeverity.Debug]: 'debug',
[CompilerErrorSeverity.Verbose]: 'verbose',
[CompilerErrorSeverity.Info]: 'info',
[CompilerErrorSeverity.Hint]: 'hint',
[CompilerErrorSeverity.Warn]: 'warn',
[CompilerErrorSeverity.Error]: 'error',
[CompilerErrorSeverity.Fatal]: 'fatal',
};
export class CompilerError {
static severity(code: number): CompilerErrorSeverity {
return code & CompilerErrorMask.Severity;
}
static error(code: number): number {
return code & CompilerErrorMask.Error;
}
static baseError(code: number): number {
return code & CompilerErrorMask.BaseError;
}
static namespace(code: number): CompilerErrorNamespace {
return code & CompilerErrorMask.Namespace;
}
static formatSeverity(code: number): string {
return errorSeverityName[CompilerError.severity(code)] ?? 'UNKNOWN';
}
/** true if events has at least one message of the atLeast severity */
static hasSeverity(events: CompilerEvent[], atLeast: CompilerErrorSeverity): boolean {
for (const { code } of events) {
if (CompilerError.severity(code) >= atLeast) {
return true;
}
}
return false;
}
/** true if events has at least one Error or worse */
static hasError(events: CompilerEvent[]): boolean {
return CompilerError.hasSeverity(events, CompilerErrorSeverity.Error);
}
/**
* Format an error code number. The error code number does not include
* the severity mask, as this is reported in text form separately; see
* `severityName`.
* @example
*
* The following call returns `KM03004`
* ```
* formatCode(CompilerMessage.ERROR_InvalidDisplayMapFile)
* ```
*/
static formatCode(code: number): string {
return Number.isInteger(code) ? 'KM' + CompilerError.error(code).toString(16).toUpperCase().padStart(5, '0') : 'KM?????';
}
/**
* Formats an event filename for an error report,
* stripping off path component
* @param filename
* @returns
*/
static formatFilename(filename: string, options?: {
fullPath?: boolean,
forwardSlashes?: boolean
}): string {
if(!filename) {
return '';
}
if(options?.fullPath) {
return options?.forwardSlashes ?
filename.replaceAll(/\\/g, '/') :
filename.replaceAll(/\//g, '\\');
}
let x = filename.lastIndexOf('/');
if(x < 0) {
x = filename.lastIndexOf('\\');
}
return x >= 0 ? filename.substring(x+1) : filename;
}
/**
* Formats an event line for an error report
* @param line
* @returns
*/
static formatLine(line: number): string {
return line ? line.toString() : '';
}
/**
* Formats an event message for an error report
* @param message
* @returns
*/
static formatMessage(message: string): string {
return message ?? '';
}
/**
* Formats a compiler message, without coloring; an ANSI color version is
* implemented in NodeCompilerCallbacks.
* @param event event or array of events
*/
static formatEvent(event : CompilerEvent | CompilerEvent[]): string {
if (!event) {
return "";
}
if (Array.isArray(event)) {
return event.map(item => CompilerError.formatEvent(item)).join('\n') + '\n';
}
return (
event.filename
? CompilerError.formatFilename(event.filename) +
(event.line ? ':' + CompilerError.formatLine(event.line) : '') + ' - '
: ''
) +
CompilerError.formatSeverity(event.code) + ' ' +
CompilerError.formatCode(event.code) + ': ' +
CompilerError.formatMessage(event.message);
}
/**
* @param e Error-like
*/
static exceptionToString(e?: any) : string {
return `${(e ?? 'unknown error').toString()}\n\nCall stack:\n${(e instanceof Error ? e.stack : (new Error()).stack)}`;
}
/**
* Returns the corresponding error severity value from a partial name match,
* e.g. 'inf' returns CompilerErrorSeverity.Info, or returns null if not found
* @param name
* @returns
*/
static severityNameToValue(name: string): CompilerErrorSeverity {
name = name.toLowerCase();
for(const level of CompilerErrorSeverityValues) {
if(errorSeverityName[level].startsWith(name)) {
return level;
}
}
return null;
}
/**
* Get an offset from o and set event's offset field
* @param event a compiler event, such as from functions in this class
* @param x any object parsed from XML or with the XML_META_DATA_SYMBOL symbol copied over
* @returns modified event object
*/
public static setFromMetadata(event: CompilerEvent, compileContext?: ObjectWithCompileContext): CompilerEvent {
if (compileContext) {
const metadata = KeymanXMLReader.getMetaData(compileContext) || {};
const offset = metadata?.startIndex;
if (offset) {
event.offset = offset;
}
const filename = event.filename || metadata[XML_FILENAME_SYMBOL];
if (filename) {
event.filename = filename;
}
}
return event;
}
};
/** @deprecated use `CompilerError.severity` instead */
export function compilerErrorSeverity(code: number): CompilerErrorSeverity {
return CompilerError.severity(code);
}
/** @deprecated use `CompilerError.formatSeverity` instead */
export function compilerErrorSeverityName(code: number): string {
return CompilerError.formatSeverity(code);
}
/** @deprecated use `CompilerError.formatCode` instead */
export function compilerErrorFormatCode(code: number): string {
return CompilerError.formatCode(code);
}
/** @deprecated use `CompilerError.formatEvent` instead */
export function compilerEventFormat(e : CompilerEvent | CompilerEvent[]) : string {
return CompilerError.formatEvent(e);
}
/**
* Defines the error code ranges for various compilers. Once defined, these
* ranges must not be changed as external modules may depend on specific error
* codes. Individual errors are defined at a compiler level, for example,
* kmc-ldml/src/compiler/messages.ts.
*
* kmc defines a mapping between each namespace and the corresponding compiler's
* error reporting class in kmc/src/messages/messageNamespaces.ts
*/
export enum CompilerErrorNamespace {
/**
* kmc-ldml errors between 0x0000…0x0FFF
*/
LdmlKeyboardCompiler = 0x0000,
/**
* common/web/utils errors between 0x1000…0x1FFF
* was previously called CommonTypes
*/
DeveloperUtils = 0x1000,
/**
* kmc-kmn errors between 0x2000…0x2FFF; these map to
* the base codes found in kmn_compiler_errors.h, exclusive severity flags
*/
KmnCompiler = 0x2000,
/**
* kmc-model errors between 0x3000…0x3FFF
*/
ModelCompiler = 0x3000,
/**
* kmc-package errors between 0x4000…0x4FFF
*/
PackageCompiler = 0x4000,
/**
* kmc and related infrastructure errors between 0x5000…0x5FFF
*/
Infrastructure = 0x5000,
/**
* kmc-analyze 0x6000…0x6FFF
*/
Analyzer = 0x6000,
/**
* kmc-kmn/kmw-compiler errors between 0x7000…0x7FFF; note that some errors
* generated by kmc-kmn/kmw-compiler are from kmc-kmn namespace for legacy
* reasons
*/
KmwCompiler = 0x7000,
/**
* kmc-model-info 0x8000…0x8FFF
*/
ModelInfoCompiler = 0x8000,
/**
* kmc-keyboard-info 0x9000…0x9FFF
*/
KeyboardInfoCompiler = 0x9000,
/**
* kmc-generate 0xA000…0xAFFF
*/
Generator = 0xA000,
/**
* kmc-copy 0xB000…0xBFFF
*/
Copier = 0xB000,
/**
* kmc-convert 0xC000…0xCFFF
*/
Converter = 0xC000,
};
type CompilerErrorSeverityOverride = CompilerErrorSeverity | 'disable';
export interface CompilerMessageOverrideMap {
[code:number]: CompilerErrorSeverityOverride;
};
export interface CompilerMessageOverride {
code: number;
level: CompilerErrorSeverityOverride;
};
export interface CompilerCallbackOptions extends CompilerBaseOptions {
// TODO: these overlap with CompilerOptions, should refactor
compilerWarningsAsErrors?: boolean;
messageOverrides?: CompilerMessageOverrideMap;
};
export interface KeymanCompilerArtifact {
data: Uint8Array;
filename: string;
};
export type KeymanCompilerArtifactOptional = KeymanCompilerArtifact | undefined;
export interface KeymanCompilerArtifacts {
readonly [type:string]: KeymanCompilerArtifactOptional;
};
export interface KeymanCompilerResult {
artifacts: KeymanCompilerArtifacts;
};
export interface KeymanCompiler {
init(callbacks: CompilerCallbacks, options: CompilerOptions): Promise<boolean>;
/**
* Run the compiler, and save the result in memory arrays. Note that while
* `outputFilename` is provided here, the output file is not written to in
* this function.
* @param inputFilename
* @param outputFilename The intended output filename, optional, if missing,
* calculated from inputFilename
* @param data
*/
run(inputFilename:string, outputFilename?:string /*, data?: any*/): Promise<KeymanCompilerResult>;
/**
* Writes the compiled output files to disk
* @param artifacts
*/
write(artifacts: KeymanCompilerArtifacts): Promise<boolean>;
};
/**
* Abstract interface for compiler options
*/
export interface CompilerBaseOptions {
/**
* Reporting level to console, used by NodeCompilerCallbacks (not used in compiler modules;
* all messages are still reported to the internal log)
*/
logLevel?: CompilerLogLevel;
/**
* Format of output for log to console
*/
logFormat?: CompilerLogFormat;
/**
* Colorize log output, default is detected from console
*/
color?: boolean;
}
export interface CompilerOptions extends CompilerBaseOptions {
/**
* Add metadata about the compiler version to .kmx file when compiling
*/
shouldAddCompilerVersion?: boolean;
/**
* Add debug information to the .kmx file when compiling
*/
saveDebug?: boolean;
/**
* Upgrade any warnings produced in the compile to errors
*/
compilerWarningsAsErrors?: boolean;
/**
* Emit warnings if deprecated code is encountered
*/
warnDeprecatedCode?: boolean;
/**
* Check filename conventions in packages
*/
checkFilenameConventions?: boolean;
};
export const defaultCompilerOptions: CompilerOptions = {
logLevel: 'info',
logFormat: 'formatted',
// outFile: (undefined)
saveDebug: false,
shouldAddCompilerVersion: true,
compilerWarningsAsErrors: false,
warnDeprecatedCode: true,
checkFilenameConventions: false,
}
/**
* Convenience function for constructing CompilerEvents
* @param code Unique numeric value of the event
* @param message A short description of the error presented to the user
* @param detail Detailed Markdown-formatted description of the error
* including references to documentation, remediation options.
* @returns
*/
export const CompilerMessageSpec = (code: number, message: string, detail?: string) : CompilerEvent => ({
code,
message,
detail,
});
/**
* Remove initial whitespace from compiler detail messages, to enable
* indented formatting of message detail strings inside the message
* definitions
* @param event
* @returns dedented event detail
*/
export function dedentCompilerMessageDetail(event: CompilerEvent) {
// TODO(lowpri): dedent may be too naive -- should use least
// non-zero whitespace line as amount to dedent
return (event.detail ?? '').replace(/^[ ]+/gm, '');
}
/**
* Convenience function for constructing CompilerEvents with line numbers.
* Use it as below: (abbreviated as mx())
*
* ```js
* // Note: Indentation makes "InvalidScanCode" line up thrice
* static ERROR_InvalidScanCode = SevError | 0x0009;
* // Note:
* // 1. All parameters are passed in 'o', the context object is only used for context even if
* // it contains redundant info.
* // 2. No code execution within the arrow function other than the 'mx' call, string interpolation,
* // with `${def(o.property)}` as the max complexity of interpolation.
* static Error_InvalidScanCode = (o:{id: string, invalidCodeList: string}, compileContext: ObjectWithCompileContext) => mx(
* this.ERROR_InvalidScanCode, compileContext,
* `Form '${def(o.id)}' has invalid/unknown scancodes '${def(o.codes)}'`,
* // Note: If detail is omitted, leave the trailing comma on the prior line to leave room for it
* `…additional markdown detail…`
* );
* ```
*
* @param code Unique numeric value of the event
* @param message A short description of the error presented to the user
* @param context Object to be used as a source for line number information
* @param detail Detailed Markdown-formatted description of the error
* including references to documentation, remediation options.
* @see CompilerMessageSpec
* @returns the event
*/
export function CompilerMessageObjectSpec(code: number, context: ObjectWithCompileContext, message: string, detail?: string): CompilerEvent {
let evt = CompilerMessageSpec(code, message, detail); // constructs raw message
evt = CompilerError.setFromMetadata(evt, context); // updates with offset from context
return evt;
};
export const CompilerMessageDef = (param: any) => String(param ?? `<param>`);
export const CompilerMessageSpecWithException = (code: number, message: string, exceptionVar: any, detail?: string) : CompilerEvent => ({
code,
message: exceptionVar
? (message ?? `Unexpected exception`) + `: ${exceptionVar.toString()}\n\nCall stack:\n${(exceptionVar instanceof Error ? exceptionVar.stack : (new Error()).stack)}` :
message,
detail,
exceptionVar,
});
/**
* Compiler logging level and correspondence to severity
*/
export const ALL_COMPILER_LOG_LEVELS = [
'silent', /// Nothing is emitted to stdout, not even errors (fatal exceptions may still emit to stdout)
'error', /// Only errors emitted
'warn', /// Errors + warnings
'hint', /// Errors + warnings + hints
'info', /// All normal messages: errors + warnings + hints + info
'verbose', /// All messages + verbose logging
'debug', /// All messages + verbose + internal debug
] as const;
type CompilerLogLevelTuple = typeof ALL_COMPILER_LOG_LEVELS;
export type CompilerLogLevel = CompilerLogLevelTuple[number];
export const compilerLogLevelToSeverity: {[index in CompilerLogLevel]: number} = {
'silent': CompilerErrorMask.Severity, // effectively excludes all reporting
'error': CompilerErrorSeverity.Error,
'warn': CompilerErrorSeverity.Warn,
'hint': CompilerErrorSeverity.Hint,
'info': CompilerErrorSeverity.Info,
'verbose': CompilerErrorSeverity.Verbose,
'debug': CompilerErrorSeverity.Debug,
};
export const ALL_COMPILER_LOG_FORMATS = [
'tsv',
'formatted'
] as const;
type CompilerLogFormatTuple = typeof ALL_COMPILER_LOG_FORMATS;
export type CompilerLogFormat = CompilerLogFormatTuple[number];