fix(developer): add missing message parameters and cleanup kmc-kmn messages further

* kmcmplib messages need to use a standard pattern for parameters, which
  cannot be named. Several of the messages had the wrong pattern
* Tidy up the kmn-compiler-messages module and make it more consistent
  to reduce risk of this happening in the future
* Add a unit test to further verify the structure of the module as far
  as possible

Fixes: #16492
Test-bot: skip
This commit is contained in:
Marc Durdin 2026-09-01 10:14:11 +02:00
parent 750ad89611
commit f438a519e1
3 changed files with 382 additions and 227 deletions

View file

@ -59,7 +59,7 @@ 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
BaseError = 0x00000FFF, // error code, 4,096 possible error codes per namespace
Reserved = 0xFF000000, // do not use these error values at this time
};

File diff suppressed because it is too large Load diff

View file

@ -3,9 +3,24 @@ import { assert } from 'chai';
import { CompilerErrorMask, CompilerErrorNamespace } from '@keymanapp/developer-utils';
import { TestCompilerCallbacks, verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers';
import { KmnCompiler } from '../src/main.js';
import { KmnCompilerMessageRanges, KmnCompilerMessages } from '../src/compiler/kmn-compiler-messages.js';
import { KmcKmnCompilerEvent, KmnCompilerMessageRanges, KmnCompilerMessages } from '../src/compiler/kmn-compiler-messages.js';
import { makePathToFixture } from './helpers/index.js';
// Source - https://stackoverflow.com/a/9924463
// Posted by Jack Allan, modified by community. See post 'Timeline' for change history
// Retrieved 2026-09-01, License - CC BY-SA 3.0
function getParamNames(func: any) {
const STRIP_COMMENTS = /(\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s*=[^,\)]*(('(?:\\'|[^'\r\n])*')|("(?:\\"|[^"\r\n])*"))|(\s*=[^,\)]*))/mg;
const ARGUMENT_NAMES = /([^\s,]+)/g;
const fnStr = func.toString().replace(STRIP_COMMENTS, '');
let result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(ARGUMENT_NAMES);
if(result === null)
result = [];
return result;
}
describe('KmnCompilerMessages', function () {
const callbacks = new TestCompilerCallbacks(this);
@ -13,6 +28,53 @@ describe('KmnCompilerMessages', function () {
return verifyCompilerMessagesObject(KmnCompilerMessages, CompilerErrorNamespace.KmnCompiler);
});
it('should have correct parameter types and numeric ranges for kmcmplib vs kmw vs kmc-kmn messages', function() {
// See comment in kmn-compiler-messages.ts for logic.
// This function follows the same pattern as `verifyCompilerMessagesObject` but with specific tests for
// this more constrained object
const toTitleCase = (s: string) => s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase();
const keys = Object.keys(KmnCompilerMessages);
const m = KmnCompilerMessages as Record<string,any>;
for(const key of keys) {
if(typeof m[key] == 'number') {
const o = /^(DEBUG|VERBOSE|INFO|HINT|WARN|ERROR|FATAL)_([A-Za-z0-9_]+)$/.exec(key);
const f = toTitleCase(o[1]) + '_' + o[2];
// note: validation done in verifyCompilerMessagesObject, don't repeat it here
const v: KmcKmnCompilerEvent = m[f]('','','','','','','','','','','','' /* ignore arguments*/);
const code = m[key] & CompilerErrorMask.BaseError;
const hex = code.toString(16).padStart(3, '0');
if(code >= KmnCompilerMessageRanges.RANGE_CompilerMessage_Min &&code <= KmnCompilerMessageRanges.RANGE_CompilerMessage_Max) {
// kmc-kmn messages, should not be using mw() function or mc() function
//
assert.equal(v.kmcKmnSource, 'kmc-kmn', `expected ${v.kmcKmnSource} to equal 'kmc-kmn' for ${hex}, '${key}'.`);
} else if(code >= KmnCompilerMessageRanges.RANGE_KMN_COMPILER_MIN && code <= KmnCompilerMessageRanges.RANGE_KMN_COMPILER_MAX) {
// kmcmplib or kmw compiler messages, should not be using m() function or mx() function
assert.oneOf(v.kmcKmnSource, ['kmcmplib', 'kmw-compiler'], `expected ${v.kmcKmnSource} to equal 'kmcmplib' or 'kmw-compiler for ${hex}, '${key}'.`);
if(v.kmcKmnSource == 'kmcmplib') {
// The only allowed param is an object o with property p. We
// cannot test parameter shape directly, so we will compare
// outputs of calling if o.p is set, which should not match what
// we get when o.p is not set.
const params = getParamNames(m[f]);
if(params.length) {
const v2: KmcKmnCompilerEvent = m[f]({p:['1','2','3','4','5','6','7','8','9']});
assert.notEqual(v.message, v2.message, `Message '${key}' (${hex}) must use KmcmpLibMessageParameters pattern`);
}
}
} else if(code >= KmnCompilerMessageRanges.RANGE_LEXICAL_MODEL_MIN && code <= KmnCompilerMessageRanges.RANGE_LEXICAL_MODEL_MAX) {
assert.fail(`Message identifier ${hex} for '${key}' is reserved and should not be used (LEXICAL_MODEL range)`);
} else {
assert.fail(`Message identifier ${hex} for '${key}' is out of range`);
}
}
}
});
it('should have a 1:1 correspondence with kmn_compiler_errors.h', function() {
const headerFilename = 'kmn_compiler_errors.h';