mirror of
https://github.com/keymanapp/keyman.git
synced 2026-09-22 15:47:41 +00:00
531 lines
17 KiB
TypeScript
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];
|