@rhost/testkit 0.1.1 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +393 -5
- package/ROADMAP.md +241 -0
- package/dist/benchmark.d.ts +44 -0
- package/dist/benchmark.d.ts.map +1 -0
- package/dist/benchmark.js +118 -0
- package/dist/benchmark.js.map +1 -0
- package/dist/cli/deploy.d.ts +2 -0
- package/dist/cli/deploy.d.ts.map +1 -0
- package/dist/cli/deploy.js +120 -0
- package/dist/cli/deploy.js.map +1 -0
- package/dist/cli/fmt.d.ts +2 -0
- package/dist/cli/fmt.d.ts.map +1 -0
- package/dist/cli/fmt.js +119 -0
- package/dist/cli/fmt.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +81 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +2 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +210 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/validate.d.ts +2 -0
- package/dist/cli/validate.d.ts.map +1 -0
- package/dist/cli/validate.js +126 -0
- package/dist/cli/validate.js.map +1 -0
- package/dist/cli/watch.d.ts +2 -0
- package/dist/cli/watch.d.ts.map +1 -0
- package/dist/cli/watch.js +136 -0
- package/dist/cli/watch.js.map +1 -0
- package/dist/client.d.ts +48 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +113 -30
- package/dist/client.js.map +1 -1
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +1 -1
- package/dist/container.js.map +1 -1
- package/dist/deployer.d.ts +86 -0
- package/dist/deployer.d.ts.map +1 -0
- package/dist/deployer.js +154 -0
- package/dist/deployer.js.map +1 -0
- package/dist/expect.d.ts +27 -1
- package/dist/expect.d.ts.map +1 -1
- package/dist/expect.js +47 -2
- package/dist/expect.js.map +1 -1
- package/dist/index.d.ts +10 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +39 -1
- package/dist/index.js.map +1 -1
- package/dist/preflight.d.ts +70 -0
- package/dist/preflight.d.ts.map +1 -0
- package/dist/preflight.js +121 -0
- package/dist/preflight.js.map +1 -0
- package/dist/reporter.d.ts +2 -0
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +24 -1
- package/dist/reporter.js.map +1 -1
- package/dist/runner.d.ts +83 -2
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +137 -8
- package/dist/runner.js.map +1 -1
- package/dist/snapshots.d.ts +84 -0
- package/dist/snapshots.d.ts.map +1 -0
- package/dist/snapshots.js +230 -0
- package/dist/snapshots.js.map +1 -0
- package/dist/validator/builtins.d.ts +18 -0
- package/dist/validator/builtins.d.ts.map +1 -0
- package/dist/validator/builtins.js +265 -0
- package/dist/validator/builtins.js.map +1 -0
- package/dist/validator/checker.d.ts +13 -0
- package/dist/validator/checker.d.ts.map +1 -0
- package/dist/validator/checker.js +111 -0
- package/dist/validator/checker.js.map +1 -0
- package/dist/validator/clobber.d.ts +7 -0
- package/dist/validator/clobber.d.ts.map +1 -0
- package/dist/validator/clobber.js +102 -0
- package/dist/validator/clobber.js.map +1 -0
- package/dist/validator/compat.d.ts +19 -0
- package/dist/validator/compat.d.ts.map +1 -0
- package/dist/validator/compat.js +58 -0
- package/dist/validator/compat.js.map +1 -0
- package/dist/validator/formatter.d.ts +21 -0
- package/dist/validator/formatter.d.ts.map +1 -0
- package/dist/validator/formatter.js +120 -0
- package/dist/validator/formatter.js.map +1 -0
- package/dist/validator/index.d.ts +57 -0
- package/dist/validator/index.d.ts.map +1 -0
- package/dist/validator/index.js +133 -0
- package/dist/validator/index.js.map +1 -0
- package/dist/validator/parser.d.ts +16 -0
- package/dist/validator/parser.d.ts.map +1 -0
- package/dist/validator/parser.js +260 -0
- package/dist/validator/parser.js.map +1 -0
- package/dist/validator/tokenizer.d.ts +28 -0
- package/dist/validator/tokenizer.d.ts.map +1 -0
- package/dist/validator/tokenizer.js +174 -0
- package/dist/validator/tokenizer.js.map +1 -0
- package/dist/validator/types.d.ts +55 -0
- package/dist/validator/types.d.ts.map +1 -0
- package/dist/validator/types.js +6 -0
- package/dist/validator/types.js.map +1 -0
- package/dist/watcher.d.ts +44 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +297 -0
- package/dist/watcher.js.map +1 -0
- package/dist/world.d.ts +79 -0
- package/dist/world.d.ts.map +1 -1
- package/dist/world.js +167 -1
- package/dist/world.js.map +1 -1
- package/package.json +19 -3
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Softcode Formatter — serialize an AST back to a normalized string
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.format = format;
|
|
7
|
+
const tokenizer_1 = require("./tokenizer");
|
|
8
|
+
const parser_1 = require("./parser");
|
|
9
|
+
/**
|
|
10
|
+
* Format a MUSHcode expression.
|
|
11
|
+
*
|
|
12
|
+
* Compact mode (default): strips extra whitespace around `(`, `,`, `)` while
|
|
13
|
+
* preserving whitespace inside argument text.
|
|
14
|
+
*
|
|
15
|
+
* Pretty mode: additionally adds newlines + indentation at each nesting level
|
|
16
|
+
* for human readability. Not suitable for direct upload to a MUSH server.
|
|
17
|
+
*/
|
|
18
|
+
function format(expr, options = {}) {
|
|
19
|
+
const tokens = (0, tokenizer_1.tokenize)(expr);
|
|
20
|
+
const { nodes } = (0, parser_1.parse)(tokens);
|
|
21
|
+
const serializer = new Serializer(options);
|
|
22
|
+
const formatted = serializer.serializeNodes(nodes, 0);
|
|
23
|
+
return { formatted, changed: formatted !== expr };
|
|
24
|
+
}
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Serializer
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
class Serializer {
|
|
29
|
+
constructor(opts) {
|
|
30
|
+
this.opts = opts;
|
|
31
|
+
}
|
|
32
|
+
serializeNodes(nodes, depth) {
|
|
33
|
+
return nodes.map(n => this.serializeNode(n, depth)).join('');
|
|
34
|
+
}
|
|
35
|
+
serializeNode(node, depth) {
|
|
36
|
+
switch (node.type) {
|
|
37
|
+
case 'FunctionCall': return this.serializeFunctionCall(node, depth);
|
|
38
|
+
case 'BracketEval': return this.serializeBracketEval(node, depth);
|
|
39
|
+
case 'Substitution': return node.raw;
|
|
40
|
+
case 'RawText': return node.value;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
serializeFunctionCall(node, depth) {
|
|
44
|
+
const name = this.opts.lowercase ? node.name.toLowerCase() : node.name;
|
|
45
|
+
if (node.args.length === 0) {
|
|
46
|
+
return `${name}()`;
|
|
47
|
+
}
|
|
48
|
+
// Trim leading/trailing RawText whitespace from each arg
|
|
49
|
+
const trimmedArgs = node.args.map(arg => trimArgWhitespace(arg));
|
|
50
|
+
const pretty = this.opts.pretty;
|
|
51
|
+
// Pretty mode: wrap if any arg contains a function call
|
|
52
|
+
if (pretty && hasNestedCall(trimmedArgs)) {
|
|
53
|
+
const indent = ' '.repeat(depth + 1);
|
|
54
|
+
const closingIndent = ' '.repeat(depth);
|
|
55
|
+
const serializedArgs = trimmedArgs.map(arg => indent + this.serializeNodes(arg, depth + 1));
|
|
56
|
+
return `${name}(\n${serializedArgs.join(',\n')}\n${closingIndent})`;
|
|
57
|
+
}
|
|
58
|
+
const serializedArgs = trimmedArgs.map(arg => this.serializeNodes(arg, depth + 1));
|
|
59
|
+
return `${name}(${serializedArgs.join(',')})`;
|
|
60
|
+
}
|
|
61
|
+
serializeBracketEval(node, depth) {
|
|
62
|
+
return `[${this.serializeNodes(node.nodes, depth)}]`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Helpers
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
/** Trim leading and trailing RawText-whitespace-only nodes from an arg list. */
|
|
69
|
+
function trimArgWhitespace(arg) {
|
|
70
|
+
if (arg.length === 0)
|
|
71
|
+
return arg;
|
|
72
|
+
let start = 0;
|
|
73
|
+
let end = arg.length - 1;
|
|
74
|
+
// Trim leading whitespace-only RawText nodes
|
|
75
|
+
while (start <= end) {
|
|
76
|
+
const node = arg[start];
|
|
77
|
+
if (node.type === 'RawText' && node.value.trim() === '') {
|
|
78
|
+
start++;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Trim trailing whitespace-only RawText nodes
|
|
85
|
+
while (end >= start) {
|
|
86
|
+
const node = arg[end];
|
|
87
|
+
if (node.type === 'RawText' && node.value.trim() === '') {
|
|
88
|
+
end--;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (start > end)
|
|
95
|
+
return [];
|
|
96
|
+
const trimmed = arg.slice(start, end + 1);
|
|
97
|
+
// Also trim leading whitespace from the value of the first RawText node
|
|
98
|
+
// (handles cases like arg starting with a non-whitespace-only RawText that begins with spaces)
|
|
99
|
+
if (trimmed.length > 0 && trimmed[0].type === 'RawText') {
|
|
100
|
+
const first = trimmed[0];
|
|
101
|
+
const trimmedValue = first.value.trimStart();
|
|
102
|
+
if (trimmedValue !== first.value) {
|
|
103
|
+
trimmed[0] = { ...first, value: trimmedValue };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// And trailing whitespace from the last RawText node
|
|
107
|
+
if (trimmed.length > 0 && trimmed[trimmed.length - 1].type === 'RawText') {
|
|
108
|
+
const last = trimmed[trimmed.length - 1];
|
|
109
|
+
const trimmedValue = last.value.trimEnd();
|
|
110
|
+
if (trimmedValue !== last.value) {
|
|
111
|
+
trimmed[trimmed.length - 1] = { ...last, value: trimmedValue };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return trimmed;
|
|
115
|
+
}
|
|
116
|
+
/** Returns true if any arg list contains a FunctionCall node (for pretty indentation decisions). */
|
|
117
|
+
function hasNestedCall(args) {
|
|
118
|
+
return args.some(arg => arg.some(node => node.type === 'FunctionCall' || node.type === 'BracketEval'));
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=formatter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formatter.js","sourceRoot":"","sources":["../../src/validator/formatter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { CompatibilityReport } from './compat';
|
|
2
|
+
import { ValidationResult } from './types';
|
|
3
|
+
export { ValidationResult, Diagnostic, Severity } from './types';
|
|
4
|
+
export { FunctionSignature, BUILTIN_FUNCTIONS, Platform } from './builtins';
|
|
5
|
+
export { CompatibilityReport, CompatibilityEntry } from './compat';
|
|
6
|
+
/**
|
|
7
|
+
* Validate a RhostMUSH softcode expression without a server connection.
|
|
8
|
+
*
|
|
9
|
+
* The validator runs three stages:
|
|
10
|
+
* 1. **Tokenizer** — converts the expression to a flat token stream
|
|
11
|
+
* 2. **Parser** — builds an AST; detects structural errors (unbalanced parens/brackets)
|
|
12
|
+
* 3. **Semantic checker** — validates function names and argument counts
|
|
13
|
+
*
|
|
14
|
+
* `valid` is `true` unless at least one diagnostic has `severity: 'error'`.
|
|
15
|
+
* Warnings do not affect validity.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* import { validate } from '@rhost/testkit/validator';
|
|
19
|
+
*
|
|
20
|
+
* const result = validate('add(2,3)');
|
|
21
|
+
* // { valid: true, diagnostics: [] }
|
|
22
|
+
*
|
|
23
|
+
* const bad = validate('add(2,3');
|
|
24
|
+
* // { valid: false, diagnostics: [{ code: 'E001', severity: 'error', ... }] }
|
|
25
|
+
*
|
|
26
|
+
* @example Catch wrong arg count
|
|
27
|
+
* validate('abs(1,2)');
|
|
28
|
+
* // { valid: false, diagnostics: [{ code: 'E007', ... }] }
|
|
29
|
+
*
|
|
30
|
+
* @example Unknown function (warning, still valid)
|
|
31
|
+
* validate('myfunc(arg)');
|
|
32
|
+
* // { valid: true, diagnostics: [{ code: 'W005', severity: 'warning', ... }] }
|
|
33
|
+
*/
|
|
34
|
+
export declare function validate(expr: string): ValidationResult;
|
|
35
|
+
/**
|
|
36
|
+
* Analyse a softcode expression for cross-platform dialect compatibility.
|
|
37
|
+
*
|
|
38
|
+
* Returns a `CompatibilityReport` listing any functions that are not
|
|
39
|
+
* available on all MUSH platforms (PennMUSH, TinyMUX, RhostMUSH).
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* const report = compatibilityReport('encode64(hello)');
|
|
43
|
+
* // report.portable === false
|
|
44
|
+
* // report.restricted === [{ name: 'encode64', platforms: ['rhost'] }]
|
|
45
|
+
*/
|
|
46
|
+
export declare function compatibilityReport(expr: string): CompatibilityReport;
|
|
47
|
+
/**
|
|
48
|
+
* Validate the softcode expression contained in a file.
|
|
49
|
+
*
|
|
50
|
+
* The entire file content is treated as a single expression.
|
|
51
|
+
* For files with multiple expressions (e.g. one per line) consider
|
|
52
|
+
* reading and validating each line individually.
|
|
53
|
+
*
|
|
54
|
+
* @throws If the file cannot be read.
|
|
55
|
+
*/
|
|
56
|
+
export declare function validateFile(filePath: string): ValidationResult;
|
|
57
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/validator/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAsB,mBAAmB,EAAsB,MAAM,UAAU,CAAC;AACvF,OAAO,EAAc,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEvD,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC5E,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAwBvD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB,CAOrE;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,CAG/D"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Public API for the RhostMUSH softcode offline validator
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.BUILTIN_FUNCTIONS = void 0;
|
|
40
|
+
exports.validate = validate;
|
|
41
|
+
exports.compatibilityReport = compatibilityReport;
|
|
42
|
+
exports.validateFile = validateFile;
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const tokenizer_1 = require("./tokenizer");
|
|
45
|
+
const parser_1 = require("./parser");
|
|
46
|
+
const checker_1 = require("./checker");
|
|
47
|
+
const clobber_1 = require("./clobber");
|
|
48
|
+
const compat_1 = require("./compat");
|
|
49
|
+
var builtins_1 = require("./builtins");
|
|
50
|
+
Object.defineProperty(exports, "BUILTIN_FUNCTIONS", { enumerable: true, get: function () { return builtins_1.BUILTIN_FUNCTIONS; } });
|
|
51
|
+
/**
|
|
52
|
+
* Validate a RhostMUSH softcode expression without a server connection.
|
|
53
|
+
*
|
|
54
|
+
* The validator runs three stages:
|
|
55
|
+
* 1. **Tokenizer** — converts the expression to a flat token stream
|
|
56
|
+
* 2. **Parser** — builds an AST; detects structural errors (unbalanced parens/brackets)
|
|
57
|
+
* 3. **Semantic checker** — validates function names and argument counts
|
|
58
|
+
*
|
|
59
|
+
* `valid` is `true` unless at least one diagnostic has `severity: 'error'`.
|
|
60
|
+
* Warnings do not affect validity.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* import { validate } from '@rhost/testkit/validator';
|
|
64
|
+
*
|
|
65
|
+
* const result = validate('add(2,3)');
|
|
66
|
+
* // { valid: true, diagnostics: [] }
|
|
67
|
+
*
|
|
68
|
+
* const bad = validate('add(2,3');
|
|
69
|
+
* // { valid: false, diagnostics: [{ code: 'E001', severity: 'error', ... }] }
|
|
70
|
+
*
|
|
71
|
+
* @example Catch wrong arg count
|
|
72
|
+
* validate('abs(1,2)');
|
|
73
|
+
* // { valid: false, diagnostics: [{ code: 'E007', ... }] }
|
|
74
|
+
*
|
|
75
|
+
* @example Unknown function (warning, still valid)
|
|
76
|
+
* validate('myfunc(arg)');
|
|
77
|
+
* // { valid: true, diagnostics: [{ code: 'W005', severity: 'warning', ... }] }
|
|
78
|
+
*/
|
|
79
|
+
function validate(expr) {
|
|
80
|
+
// Empty expression: valid but warn
|
|
81
|
+
if (expr.trim() === '') {
|
|
82
|
+
const diagnostics = [
|
|
83
|
+
{
|
|
84
|
+
severity: 'warning',
|
|
85
|
+
code: 'W001',
|
|
86
|
+
message: 'Expression is empty',
|
|
87
|
+
offset: 0,
|
|
88
|
+
length: 0,
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
return { valid: true, diagnostics };
|
|
92
|
+
}
|
|
93
|
+
const tokens = (0, tokenizer_1.tokenize)(expr);
|
|
94
|
+
const { nodes, diagnostics: structural } = (0, parser_1.parse)(tokens);
|
|
95
|
+
const semantic = (0, checker_1.semanticCheck)(nodes);
|
|
96
|
+
const clobber = (0, clobber_1.registerClobberCheck)(nodes);
|
|
97
|
+
const all = [...structural, ...semantic, ...clobber];
|
|
98
|
+
const valid = !all.some((d) => d.severity === 'error');
|
|
99
|
+
return { valid, diagnostics: all };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Analyse a softcode expression for cross-platform dialect compatibility.
|
|
103
|
+
*
|
|
104
|
+
* Returns a `CompatibilityReport` listing any functions that are not
|
|
105
|
+
* available on all MUSH platforms (PennMUSH, TinyMUX, RhostMUSH).
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* const report = compatibilityReport('encode64(hello)');
|
|
109
|
+
* // report.portable === false
|
|
110
|
+
* // report.restricted === [{ name: 'encode64', platforms: ['rhost'] }]
|
|
111
|
+
*/
|
|
112
|
+
function compatibilityReport(expr) {
|
|
113
|
+
if (expr.trim() === '') {
|
|
114
|
+
return { restricted: [], portable: true };
|
|
115
|
+
}
|
|
116
|
+
const tokens = (0, tokenizer_1.tokenize)(expr);
|
|
117
|
+
const { nodes } = (0, parser_1.parse)(tokens);
|
|
118
|
+
return (0, compat_1.compatibilityCheck)(nodes);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Validate the softcode expression contained in a file.
|
|
122
|
+
*
|
|
123
|
+
* The entire file content is treated as a single expression.
|
|
124
|
+
* For files with multiple expressions (e.g. one per line) consider
|
|
125
|
+
* reading and validating each line individually.
|
|
126
|
+
*
|
|
127
|
+
* @throws If the file cannot be read.
|
|
128
|
+
*/
|
|
129
|
+
function validateFile(filePath) {
|
|
130
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
131
|
+
return validate(content);
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/validator/index.ts"],"names":[],"mappings":";AAAA,8EAA8E;AAC9E,0DAA0D;AAC1D,8EAA8E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0C9E,4BAwBC;AAaD,kDAOC;AAWD,oCAGC;AAlGD,uCAAyB;AACzB,2CAAuC;AACvC,qCAAiC;AACjC,uCAA0C;AAC1C,uCAAiD;AACjD,qCAAuF;AAIvF,uCAA4E;AAAhD,6GAAA,iBAAiB,OAAA;AAG7C;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,SAAgB,QAAQ,CAAC,IAAY;IACnC,mCAAmC;IACnC,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACvB,MAAM,WAAW,GAAiB;YAChC;gBACE,QAAQ,EAAE,SAAS;gBACnB,IAAI,EAAE,MAAM;gBACZ,OAAO,EAAE,qBAAqB;gBAC9B,MAAM,EAAE,CAAC;gBACT,MAAM,EAAE,CAAC;aACV;SACF,CAAC;QACF,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;IACtC,CAAC;IAED,MAAM,MAAM,GAAG,IAAA,oBAAQ,EAAC,IAAI,CAAC,CAAC;IAC9B,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,GAAG,IAAA,cAAK,EAAC,MAAM,CAAC,CAAC;IACzD,MAAM,QAAQ,GAAG,IAAA,uBAAa,EAAC,KAAK,CAAC,CAAC;IACtC,MAAM,OAAO,GAAG,IAAA,8BAAoB,EAAC,KAAK,CAAC,CAAC;IAE5C,MAAM,GAAG,GAAG,CAAC,GAAG,UAAU,EAAE,GAAG,QAAQ,EAAE,GAAG,OAAO,CAAC,CAAC;IACrD,MAAM,KAAK,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC;IAEvD,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC;AACrC,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAgB,mBAAmB,CAAC,IAAY;IAC9C,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACvB,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC5C,CAAC;IACD,MAAM,MAAM,GAAG,IAAA,oBAAQ,EAAC,IAAI,CAAC,CAAC;IAC9B,MAAM,EAAE,KAAK,EAAE,GAAG,IAAA,cAAK,EAAC,MAAM,CAAC,CAAC;IAChC,OAAO,IAAA,2BAAkB,EAAC,KAAK,CAAC,CAAC;AACnC,CAAC;AAED;;;;;;;;GAQG;AACH,SAAgB,YAAY,CAAC,QAAgB;IAC3C,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACnD,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC;AAC3B,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Token } from './tokenizer';
|
|
2
|
+
import { ASTNode, Diagnostic } from './types';
|
|
3
|
+
export interface ParseResult {
|
|
4
|
+
nodes: ASTNode[];
|
|
5
|
+
/** Structural diagnostics: unclosed parens/brackets, stray ), ], etc. */
|
|
6
|
+
diagnostics: Diagnostic[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Parse a flat token stream into an AST.
|
|
10
|
+
*
|
|
11
|
+
* Structural errors (unbalanced parens/brackets) are embedded in `diagnostics`.
|
|
12
|
+
* The returned `nodes` array always contains the best-effort parse even when
|
|
13
|
+
* there are errors, enabling downstream semantic checks to still run.
|
|
14
|
+
*/
|
|
15
|
+
export declare function parse(tokens: Token[]): ParseResult;
|
|
16
|
+
//# sourceMappingURL=parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/validator/parser.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,OAAO,EAAmB,UAAU,EAAmD,MAAM,SAAS,CAAC;AAEhH,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,yEAAyE;IACzE,WAAW,EAAE,UAAU,EAAE,CAAC;CAC3B;AA4RD;;;;;;GAMG;AACH,wBAAgB,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,WAAW,CAIlD"}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// RhostMUSH softcode recursive-descent parser
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.parse = parse;
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Parser class
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
class Parser {
|
|
11
|
+
constructor(tokens) {
|
|
12
|
+
this.tokens = tokens;
|
|
13
|
+
this.pos = 0;
|
|
14
|
+
this.diagnostics = [];
|
|
15
|
+
}
|
|
16
|
+
peek() {
|
|
17
|
+
return this.pos < this.tokens.length ? this.tokens[this.pos] : null;
|
|
18
|
+
}
|
|
19
|
+
consume() {
|
|
20
|
+
if (this.pos >= this.tokens.length) {
|
|
21
|
+
throw new Error('Unexpected end of token stream');
|
|
22
|
+
}
|
|
23
|
+
return this.tokens[this.pos++];
|
|
24
|
+
}
|
|
25
|
+
// -------------------------------------------------------------------------
|
|
26
|
+
// Top-level entry point
|
|
27
|
+
// -------------------------------------------------------------------------
|
|
28
|
+
/**
|
|
29
|
+
* Parse the entire token stream as a top-level expression.
|
|
30
|
+
* Stray `)` and `]` at the top level are reported as errors.
|
|
31
|
+
* Top-level commas are treated as literal text.
|
|
32
|
+
*/
|
|
33
|
+
parseTopLevel() {
|
|
34
|
+
const nodes = [];
|
|
35
|
+
while (this.peek() !== null) {
|
|
36
|
+
const tok = this.peek();
|
|
37
|
+
if (tok.type === 'RPAREN') {
|
|
38
|
+
this.diagnostics.push({
|
|
39
|
+
severity: 'error',
|
|
40
|
+
code: 'E002',
|
|
41
|
+
message: `Unexpected ')' — no matching '(' was opened`,
|
|
42
|
+
offset: tok.offset,
|
|
43
|
+
length: 1,
|
|
44
|
+
});
|
|
45
|
+
this.consume();
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (tok.type === 'RBRACKET') {
|
|
49
|
+
this.diagnostics.push({
|
|
50
|
+
severity: 'error',
|
|
51
|
+
code: 'E004',
|
|
52
|
+
message: `Unexpected ']' — no matching '[' was opened`,
|
|
53
|
+
offset: tok.offset,
|
|
54
|
+
length: 1,
|
|
55
|
+
});
|
|
56
|
+
this.consume();
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (tok.type === 'COMMA') {
|
|
60
|
+
// Top-level comma is literal text (not inside a function call)
|
|
61
|
+
nodes.push({ type: 'RawText', value: ',', offset: tok.offset });
|
|
62
|
+
this.consume();
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const node = this.parseNode();
|
|
66
|
+
if (node !== null)
|
|
67
|
+
nodes.push(node);
|
|
68
|
+
}
|
|
69
|
+
return nodes;
|
|
70
|
+
}
|
|
71
|
+
// -------------------------------------------------------------------------
|
|
72
|
+
// Node dispatch
|
|
73
|
+
// -------------------------------------------------------------------------
|
|
74
|
+
/**
|
|
75
|
+
* Parse a single AST node. Returns null only at EOF.
|
|
76
|
+
*/
|
|
77
|
+
parseNode() {
|
|
78
|
+
const tok = this.peek();
|
|
79
|
+
if (tok === null)
|
|
80
|
+
return null;
|
|
81
|
+
if (tok.type === 'FNAME') {
|
|
82
|
+
return this.parseFunctionCall();
|
|
83
|
+
}
|
|
84
|
+
if (tok.type === 'LBRACKET') {
|
|
85
|
+
return this.parseBracketEval();
|
|
86
|
+
}
|
|
87
|
+
if (tok.type === 'SUBST') {
|
|
88
|
+
this.consume();
|
|
89
|
+
return { type: 'Substitution', raw: tok.value, offset: tok.offset };
|
|
90
|
+
}
|
|
91
|
+
// ESC, TEXT, LPAREN (bare paren not after identifier), anything else → RawText
|
|
92
|
+
this.consume();
|
|
93
|
+
return { type: 'RawText', value: tok.value, offset: tok.offset };
|
|
94
|
+
}
|
|
95
|
+
// -------------------------------------------------------------------------
|
|
96
|
+
// Function call: FNAME LPAREN [arglist] RPAREN
|
|
97
|
+
// -------------------------------------------------------------------------
|
|
98
|
+
parseFunctionCall() {
|
|
99
|
+
const nameTok = this.consume(); // FNAME
|
|
100
|
+
// Tokenizer guarantees LPAREN immediately follows FNAME
|
|
101
|
+
const lparenTok = this.consume(); // LPAREN
|
|
102
|
+
const node = {
|
|
103
|
+
type: 'FunctionCall',
|
|
104
|
+
name: nameTok.value,
|
|
105
|
+
nameOffset: nameTok.offset,
|
|
106
|
+
args: [],
|
|
107
|
+
offset: lparenTok.offset,
|
|
108
|
+
};
|
|
109
|
+
// Immediately closed: name()
|
|
110
|
+
if (this.peek()?.type === 'RPAREN') {
|
|
111
|
+
this.consume();
|
|
112
|
+
return node;
|
|
113
|
+
}
|
|
114
|
+
// EOF immediately after opening paren
|
|
115
|
+
if (this.peek() === null) {
|
|
116
|
+
this.diagnostics.push(this.unclosedParen(nameTok.value, lparenTok.offset));
|
|
117
|
+
return node;
|
|
118
|
+
}
|
|
119
|
+
// Parse comma-separated argument expressions
|
|
120
|
+
while (true) {
|
|
121
|
+
const arg = this.parseArgExpression(nameTok.value);
|
|
122
|
+
node.args.push(arg);
|
|
123
|
+
const next = this.peek();
|
|
124
|
+
if (next === null) {
|
|
125
|
+
// EOF without closing paren
|
|
126
|
+
this.diagnostics.push(this.unclosedParen(nameTok.value, lparenTok.offset));
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
if (next.type === 'RPAREN') {
|
|
130
|
+
this.consume();
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
if (next.type === 'COMMA') {
|
|
134
|
+
this.consume();
|
|
135
|
+
// Trailing comma — push one final empty arg and close on next RPAREN
|
|
136
|
+
if (this.peek()?.type === 'RPAREN') {
|
|
137
|
+
node.args.push([]);
|
|
138
|
+
this.consume();
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
if (this.peek() === null) {
|
|
142
|
+
node.args.push([]);
|
|
143
|
+
this.diagnostics.push(this.unclosedParen(nameTok.value, lparenTok.offset));
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
// Unexpected token — shouldn't happen with a well-formed token stream
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
return node;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Parse one argument expression, stopping (without consuming) at a
|
|
155
|
+
* COMMA or RPAREN that closes the current function call.
|
|
156
|
+
* LBRACKET/RBRACKET are handled recursively by parseBracketEval, so
|
|
157
|
+
* they do NOT prematurely end an argument even if they contain commas.
|
|
158
|
+
*/
|
|
159
|
+
parseArgExpression(funcName) {
|
|
160
|
+
const nodes = [];
|
|
161
|
+
while (true) {
|
|
162
|
+
const tok = this.peek();
|
|
163
|
+
if (tok === null)
|
|
164
|
+
break;
|
|
165
|
+
// These end the current argument
|
|
166
|
+
if (tok.type === 'COMMA' || tok.type === 'RPAREN')
|
|
167
|
+
break;
|
|
168
|
+
// Stray ] inside a function argument
|
|
169
|
+
if (tok.type === 'RBRACKET') {
|
|
170
|
+
this.diagnostics.push({
|
|
171
|
+
severity: 'error',
|
|
172
|
+
code: 'E004',
|
|
173
|
+
message: `Unexpected ']' inside argument of '${funcName}'`,
|
|
174
|
+
offset: tok.offset,
|
|
175
|
+
length: 1,
|
|
176
|
+
});
|
|
177
|
+
this.consume();
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const node = this.parseNode();
|
|
181
|
+
if (node !== null)
|
|
182
|
+
nodes.push(node);
|
|
183
|
+
}
|
|
184
|
+
return nodes;
|
|
185
|
+
}
|
|
186
|
+
// -------------------------------------------------------------------------
|
|
187
|
+
// Bracket eval: LBRACKET expression RBRACKET
|
|
188
|
+
// -------------------------------------------------------------------------
|
|
189
|
+
parseBracketEval() {
|
|
190
|
+
const lbracketTok = this.consume(); // LBRACKET
|
|
191
|
+
const innerNodes = [];
|
|
192
|
+
while (true) {
|
|
193
|
+
const tok = this.peek();
|
|
194
|
+
if (tok === null) {
|
|
195
|
+
this.diagnostics.push({
|
|
196
|
+
severity: 'error',
|
|
197
|
+
code: 'E003',
|
|
198
|
+
message: `Unclosed '[' — missing closing ']'`,
|
|
199
|
+
offset: lbracketTok.offset,
|
|
200
|
+
length: 1,
|
|
201
|
+
});
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
if (tok.type === 'RBRACKET') {
|
|
205
|
+
this.consume();
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
// Stray ) inside a bracket eval at the bracket's top level
|
|
209
|
+
if (tok.type === 'RPAREN') {
|
|
210
|
+
this.diagnostics.push({
|
|
211
|
+
severity: 'error',
|
|
212
|
+
code: 'E002',
|
|
213
|
+
message: `Unexpected ')' inside '[ ]' expression`,
|
|
214
|
+
offset: tok.offset,
|
|
215
|
+
length: 1,
|
|
216
|
+
});
|
|
217
|
+
this.consume();
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
// Top-level comma inside a bracket eval is literal text
|
|
221
|
+
if (tok.type === 'COMMA') {
|
|
222
|
+
innerNodes.push({ type: 'RawText', value: ',', offset: tok.offset });
|
|
223
|
+
this.consume();
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const node = this.parseNode();
|
|
227
|
+
if (node !== null)
|
|
228
|
+
innerNodes.push(node);
|
|
229
|
+
}
|
|
230
|
+
return { type: 'BracketEval', nodes: innerNodes, offset: lbracketTok.offset };
|
|
231
|
+
}
|
|
232
|
+
// -------------------------------------------------------------------------
|
|
233
|
+
// Helpers
|
|
234
|
+
// -------------------------------------------------------------------------
|
|
235
|
+
unclosedParen(funcName, offset) {
|
|
236
|
+
return {
|
|
237
|
+
severity: 'error',
|
|
238
|
+
code: 'E001',
|
|
239
|
+
message: `Unclosed '(' for function '${funcName}' — missing closing ')'`,
|
|
240
|
+
offset,
|
|
241
|
+
length: 1,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// Public entry point
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
/**
|
|
249
|
+
* Parse a flat token stream into an AST.
|
|
250
|
+
*
|
|
251
|
+
* Structural errors (unbalanced parens/brackets) are embedded in `diagnostics`.
|
|
252
|
+
* The returned `nodes` array always contains the best-effort parse even when
|
|
253
|
+
* there are errors, enabling downstream semantic checks to still run.
|
|
254
|
+
*/
|
|
255
|
+
function parse(tokens) {
|
|
256
|
+
const parser = new Parser(tokens);
|
|
257
|
+
const nodes = parser.parseTopLevel();
|
|
258
|
+
return { nodes, diagnostics: parser.diagnostics };
|
|
259
|
+
}
|
|
260
|
+
//# sourceMappingURL=parser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parser.js","sourceRoot":"","sources":["../../src/validator/parser.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type TokenType = 'FNAME' | 'LPAREN' | 'RPAREN' | 'COMMA' | 'LBRACKET' | 'RBRACKET' | 'SUBST' | 'ESC' | 'TEXT';
|
|
2
|
+
export interface Token {
|
|
3
|
+
type: TokenType;
|
|
4
|
+
/** The raw text of this token as it appears in the source */
|
|
5
|
+
value: string;
|
|
6
|
+
/** Character offset in the original expression string */
|
|
7
|
+
offset: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Tokenize a RhostMUSH softcode expression into a flat token stream.
|
|
11
|
+
*
|
|
12
|
+
* Rules (in priority order):
|
|
13
|
+
* 1. `\X` → ESC(X) — escape always wins
|
|
14
|
+
* 2. `%%` → TEXT('%') — literal percent
|
|
15
|
+
* 3. `%qX` / `%iX` → SUBST('%qX') — 3-char register subst
|
|
16
|
+
* 4. `%X` (known) → SUBST('%X') — 2-char substitution
|
|
17
|
+
* 5. `%` (unknown) → TEXT('%') — treat as literal
|
|
18
|
+
* 6. `[` → LBRACKET
|
|
19
|
+
* 7. `]` → RBRACKET
|
|
20
|
+
* 8. `)` → RPAREN
|
|
21
|
+
* 9. `,` → COMMA
|
|
22
|
+
* 10. `word(` → FNAME(word) + LPAREN('(')
|
|
23
|
+
* 11. `word` → TEXT(word) — identifier not before (
|
|
24
|
+
* 12. `(` → TEXT('(') — ( not after identifier
|
|
25
|
+
* 13. <run> → TEXT(run) — anything else
|
|
26
|
+
*/
|
|
27
|
+
export declare function tokenize(input: string): Token[];
|
|
28
|
+
//# sourceMappingURL=tokenizer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokenizer.d.ts","sourceRoot":"","sources":["../../src/validator/tokenizer.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,SAAS,GACjB,OAAO,GACP,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,UAAU,GACV,UAAU,GACV,OAAO,GACP,KAAK,GACL,MAAM,CAAC;AAEX,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,SAAS,CAAC;IAChB,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAC;CAChB;AAsCD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,EAAE,CAoI/C"}
|