@rhost/testkit 1.5.0 → 1.5.2
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/node_modules/@ursamu/mushcode/.github/workflows/publish.yml +36 -0
- package/node_modules/@ursamu/mushcode/LICENSE +21 -0
- package/node_modules/@ursamu/mushcode/README.md +110 -0
- package/node_modules/@ursamu/mushcode/_dist/mod.d.ts +36 -0
- package/node_modules/@ursamu/mushcode/_dist/mod.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/parser/mod.d.ts +41 -0
- package/node_modules/@ursamu/mushcode/_dist/parser/mod.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/src/analyze/commands.d.ts +15 -0
- package/node_modules/@ursamu/mushcode/_dist/src/analyze/commands.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/src/analyze/deps.d.ts +18 -0
- package/node_modules/@ursamu/mushcode/_dist/src/analyze/deps.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/src/analyze/mod.d.ts +20 -0
- package/node_modules/@ursamu/mushcode/_dist/src/analyze/mod.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/src/analyze/tags.d.ts +6 -0
- package/node_modules/@ursamu/mushcode/_dist/src/analyze/tags.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/src/eval/context.d.ts +85 -0
- package/node_modules/@ursamu/mushcode/_dist/src/eval/context.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/src/eval/engine.d.ts +48 -0
- package/node_modules/@ursamu/mushcode/_dist/src/eval/engine.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/src/eval/mod.d.ts +26 -0
- package/node_modules/@ursamu/mushcode/_dist/src/eval/mod.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/src/eval/stdlib/mod.d.ts +3 -0
- package/node_modules/@ursamu/mushcode/_dist/src/eval/stdlib/mod.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/src/lint/mod.d.ts +38 -0
- package/node_modules/@ursamu/mushcode/_dist/src/lint/mod.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/src/print/mod.d.ts +18 -0
- package/node_modules/@ursamu/mushcode/_dist/src/print/mod.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/src/print/printer.d.ts +15 -0
- package/node_modules/@ursamu/mushcode/_dist/src/print/printer.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/src/traverse/mod.d.ts +19 -0
- package/node_modules/@ursamu/mushcode/_dist/src/traverse/mod.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/src/traverse/transform.d.ts +27 -0
- package/node_modules/@ursamu/mushcode/_dist/src/traverse/transform.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/src/traverse/walk.d.ts +27 -0
- package/node_modules/@ursamu/mushcode/_dist/src/traverse/walk.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/deno.json +26 -0
- package/node_modules/@ursamu/mushcode/deno.lock +42 -0
- package/node_modules/@ursamu/mushcode/docs/analyze.md +145 -0
- package/node_modules/@ursamu/mushcode/docs/eval.md +312 -0
- package/node_modules/@ursamu/mushcode/docs/lint.md +152 -0
- package/node_modules/@ursamu/mushcode/docs/parser.md +196 -0
- package/node_modules/@ursamu/mushcode/docs/print.md +84 -0
- package/node_modules/@ursamu/mushcode/docs/stdlib.md +418 -0
- package/node_modules/@ursamu/mushcode/docs/traverse.md +167 -0
- package/node_modules/@ursamu/mushcode/grammar/mux-softcode.pegjs +781 -0
- package/node_modules/@ursamu/mushcode/mod.js +44 -0
- package/node_modules/@ursamu/mushcode/mod.js.map +1 -0
- package/node_modules/@ursamu/mushcode/mod.ts +63 -0
- package/node_modules/@ursamu/mushcode/package.json +38 -0
- package/node_modules/@ursamu/mushcode/parser/mod.js +47 -0
- package/node_modules/@ursamu/mushcode/parser/mod.js.map +1 -0
- package/node_modules/@ursamu/mushcode/parser/mod.ts +99 -0
- package/node_modules/@ursamu/mushcode/parser/mux-softcode.js +3833 -0
- package/node_modules/@ursamu/mushcode/parser/mux-softcode.mjs +3837 -0
- package/node_modules/@ursamu/mushcode/src/analyze/commands.js +29 -0
- package/node_modules/@ursamu/mushcode/src/analyze/commands.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/analyze/commands.ts +46 -0
- package/node_modules/@ursamu/mushcode/src/analyze/deps.js +45 -0
- package/node_modules/@ursamu/mushcode/src/analyze/deps.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/analyze/deps.ts +51 -0
- package/node_modules/@ursamu/mushcode/src/analyze/mod.js +18 -0
- package/node_modules/@ursamu/mushcode/src/analyze/mod.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/analyze/mod.ts +20 -0
- package/node_modules/@ursamu/mushcode/src/analyze/tags.js +11 -0
- package/node_modules/@ursamu/mushcode/src/analyze/tags.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/analyze/tags.ts +11 -0
- package/node_modules/@ursamu/mushcode/src/eval/context.js +22 -0
- package/node_modules/@ursamu/mushcode/src/eval/context.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/eval/context.ts +177 -0
- package/node_modules/@ursamu/mushcode/src/eval/engine.js +238 -0
- package/node_modules/@ursamu/mushcode/src/eval/engine.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/eval/engine.ts +276 -0
- package/node_modules/@ursamu/mushcode/src/eval/mod.js +25 -0
- package/node_modules/@ursamu/mushcode/src/eval/mod.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/eval/mod.ts +31 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/compare.js +56 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/compare.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/compare.ts +16 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/db.js +91 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/db.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/db.ts +104 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/iter.js +91 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/iter.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/iter.ts +98 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/logic.js +79 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/logic.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/logic.ts +84 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/math.js +120 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/math.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/math.ts +115 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/mod.js +17 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/mod.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/mod.ts +19 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/register.js +28 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/register.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/register.ts +31 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/string.js +153 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/string.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/string.ts +154 -0
- package/node_modules/@ursamu/mushcode/src/lint/builtin_arities.js +212 -0
- package/node_modules/@ursamu/mushcode/src/lint/builtin_arities.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/lint/builtin_arities.ts +68 -0
- package/node_modules/@ursamu/mushcode/src/lint/mod.js +60 -0
- package/node_modules/@ursamu/mushcode/src/lint/mod.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/lint/mod.ts +96 -0
- package/node_modules/@ursamu/mushcode/src/lint/rules/arg_count.js +37 -0
- package/node_modules/@ursamu/mushcode/src/lint/rules/arg_count.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/lint/rules/arg_count.ts +44 -0
- package/node_modules/@ursamu/mushcode/src/lint/rules/iter_var_outside_iter.js +55 -0
- package/node_modules/@ursamu/mushcode/src/lint/rules/iter_var_outside_iter.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/lint/rules/iter_var_outside_iter.ts +60 -0
- package/node_modules/@ursamu/mushcode/src/lint/rules/missing_wildcard.js +31 -0
- package/node_modules/@ursamu/mushcode/src/lint/rules/missing_wildcard.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/lint/rules/missing_wildcard.ts +40 -0
- package/node_modules/@ursamu/mushcode/src/lint/rules/register_before_set.js +59 -0
- package/node_modules/@ursamu/mushcode/src/lint/rules/register_before_set.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/lint/rules/register_before_set.ts +64 -0
- package/node_modules/@ursamu/mushcode/src/print/lock_printer.js +43 -0
- package/node_modules/@ursamu/mushcode/src/print/lock_printer.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/print/lock_printer.ts +41 -0
- package/node_modules/@ursamu/mushcode/src/print/mod.js +17 -0
- package/node_modules/@ursamu/mushcode/src/print/mod.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/print/mod.ts +18 -0
- package/node_modules/@ursamu/mushcode/src/print/printer.js +91 -0
- package/node_modules/@ursamu/mushcode/src/print/printer.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/print/printer.ts +132 -0
- package/node_modules/@ursamu/mushcode/src/traverse/child_slots.js +129 -0
- package/node_modules/@ursamu/mushcode/src/traverse/child_slots.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/traverse/child_slots.ts +51 -0
- package/node_modules/@ursamu/mushcode/src/traverse/mod.js +17 -0
- package/node_modules/@ursamu/mushcode/src/traverse/mod.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/traverse/mod.ts +19 -0
- package/node_modules/@ursamu/mushcode/src/traverse/transform.js +70 -0
- package/node_modules/@ursamu/mushcode/src/traverse/transform.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/traverse/transform.ts +84 -0
- package/node_modules/@ursamu/mushcode/src/traverse/walk.js +55 -0
- package/node_modules/@ursamu/mushcode/src/traverse/walk.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/traverse/walk.ts +82 -0
- package/node_modules/@ursamu/mushcode/tests/01-literals.test.ts +105 -0
- package/node_modules/@ursamu/mushcode/tests/02-substitutions.test.ts +145 -0
- package/node_modules/@ursamu/mushcode/tests/03-function-calls.test.ts +184 -0
- package/node_modules/@ursamu/mushcode/tests/04-eval-blocks.test.ts +110 -0
- package/node_modules/@ursamu/mushcode/tests/05-braced-strings.test.ts +119 -0
- package/node_modules/@ursamu/mushcode/tests/06-commands.test.ts +222 -0
- package/node_modules/@ursamu/mushcode/tests/07-dollar-patterns.test.ts +156 -0
- package/node_modules/@ursamu/mushcode/tests/08-lock-expressions.test.ts +159 -0
- package/node_modules/@ursamu/mushcode/tests/09-edge-cases.test.ts +162 -0
- package/node_modules/@ursamu/mushcode/tests/10-regression.test.ts +211 -0
- package/node_modules/@ursamu/mushcode/tests/11-tags.test.ts +357 -0
- package/node_modules/@ursamu/mushcode/tests/12-locations.test.ts +162 -0
- package/node_modules/@ursamu/mushcode/tests/13-eval.test.ts +389 -0
- package/node_modules/@ursamu/mushcode/tests/analyze.test.ts +194 -0
- package/node_modules/@ursamu/mushcode/tests/helpers.ts +69 -0
- package/node_modules/@ursamu/mushcode/tests/lint.test.ts +232 -0
- package/node_modules/@ursamu/mushcode/tests/print.test.ts +204 -0
- package/node_modules/@ursamu/mushcode/tests/traverse.test.ts +211 -0
- package/package.json +4 -1
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// 11 — RhostMUSH tag system: @tag/@ltag commands, #tagname TagRef nodes,
|
|
3
|
+
// tag()/listtags()/tagmatch() functions
|
|
4
|
+
//
|
|
5
|
+
// Sources: RhostMUSH trunk Server/src/object.c (objecttag_*),
|
|
6
|
+
// Server/src/command.c (@tag/@ltag registration),
|
|
7
|
+
// Server/src/functions.c (fun_tag, fun_listtags, fun_tagmatch)
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
import { assertEquals, assertNotEquals } from "jsr:@std/assert@^1";
|
|
11
|
+
import { describe, it } from "jsr:/@std/testing@^1/bdd";
|
|
12
|
+
import { mustParse, findAll, findFirst } from "./helpers.ts";
|
|
13
|
+
|
|
14
|
+
// ── TagRef node ───────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
describe("TagRef — #tagname syntax", () => {
|
|
17
|
+
it("#weather → TagRef(name='weather')", () => {
|
|
18
|
+
const ast = mustParse("#weather");
|
|
19
|
+
const tags = findAll(ast, "TagRef");
|
|
20
|
+
assertEquals(tags.length, 1);
|
|
21
|
+
assertEquals(tags[0].name, "weather");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("#globals → TagRef(name='globals')", () => {
|
|
25
|
+
const tags = findAll(mustParse("#globals"), "TagRef");
|
|
26
|
+
assertEquals(tags[0].name, "globals");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("#_localdb → TagRef(name='_localdb') — underscore-prefixed personal tag", () => {
|
|
30
|
+
const tags = findAll(mustParse("#_localdb"), "TagRef");
|
|
31
|
+
assertEquals(tags.length, 1);
|
|
32
|
+
assertEquals(tags[0].name, "_localdb");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("#my-tag → TagRef(name='my-tag') — hyphen in tag name", () => {
|
|
36
|
+
const tags = findAll(mustParse("#my-tag"), "TagRef");
|
|
37
|
+
assertEquals(tags[0].name, "my-tag");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("#weather_data → TagRef (underscore in name)", () => {
|
|
41
|
+
const tags = findAll(mustParse("#weather_data"), "TagRef");
|
|
42
|
+
assertEquals(tags[0].name, "weather_data");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("TagRef disambiguation — must NOT become TagRef", () => {
|
|
47
|
+
it("#123 stays as Literal '#123' (numeric dbref)", () => {
|
|
48
|
+
const ast = mustParse("#123");
|
|
49
|
+
const tags = findAll(ast, "TagRef");
|
|
50
|
+
assertEquals(tags.length, 0);
|
|
51
|
+
// coalesce merges '#' + '123' into one Literal
|
|
52
|
+
const lits = findAll(ast, "Literal");
|
|
53
|
+
// deno-lint-ignore no-explicit-any
|
|
54
|
+
const combined = lits.map((l) => (l as any).value).join("");
|
|
55
|
+
assertEquals(combined, "#123");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("## stays as SpecialVar '##' (iter current)", () => {
|
|
59
|
+
const sv = findAll(mustParse("##"), "SpecialVar");
|
|
60
|
+
assertEquals(sv.length, 1);
|
|
61
|
+
assertEquals(sv[0].code, "##");
|
|
62
|
+
assertEquals(findAll(mustParse("##"), "TagRef").length, 0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("#@ stays as SpecialVar '#@'", () => {
|
|
66
|
+
const sv = findAll(mustParse("#@"), "SpecialVar");
|
|
67
|
+
assertEquals(sv[0].code, "#@");
|
|
68
|
+
assertEquals(findAll(mustParse("#@"), "TagRef").length, 0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("#$ stays as SpecialVar '#$'", () => {
|
|
72
|
+
const sv = findAll(mustParse("#$"), "SpecialVar");
|
|
73
|
+
assertEquals(sv[0].code, "#$");
|
|
74
|
+
assertEquals(findAll(mustParse("#$"), "TagRef").length, 0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("bare # not followed by identifier stays Literal", () => {
|
|
78
|
+
const ast = mustParse("obj # here");
|
|
79
|
+
assertEquals(findAll(ast, "TagRef").length, 0);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ── TagRef in various positions ───────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
describe("TagRef in command/object positions", () => {
|
|
86
|
+
it("@trigger #weather/ATTR=hello → TagRef in @trigger object", () => {
|
|
87
|
+
const ast = mustParse("@trigger #weather/ATTR=hello");
|
|
88
|
+
assertEquals(ast.type, "AtCommand");
|
|
89
|
+
assertEquals(ast.name, "trigger");
|
|
90
|
+
const tags = findAll(ast.object, "TagRef");
|
|
91
|
+
assertEquals(tags.length, 1);
|
|
92
|
+
assertEquals(tags[0].name, "weather");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("@set #globals=MAGIC → TagRef in @set object", () => {
|
|
96
|
+
const ast = mustParse("@set #globals=MAGIC");
|
|
97
|
+
assertEquals(ast.type, "AtCommand");
|
|
98
|
+
const tags = findAll(ast.object, "TagRef");
|
|
99
|
+
assertEquals(tags[0].name, "globals");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("#weather in value position → TagRef in command value", () => {
|
|
103
|
+
const ast = mustParse("@pemit %#=#weather");
|
|
104
|
+
assertEquals(ast.type, "AtCommand");
|
|
105
|
+
const tags = findAll(ast.value, "TagRef");
|
|
106
|
+
assertEquals(tags.length, 1);
|
|
107
|
+
assertEquals(tags[0].name, "weather");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("@trigger #db/EVENT=%0 → TagRef + literal /EVENT in object", () => {
|
|
111
|
+
const ast = mustParse("@trigger #db/EVENT=%0");
|
|
112
|
+
const tags = findAll(ast.object, "TagRef");
|
|
113
|
+
assertEquals(tags.length, 1);
|
|
114
|
+
assertEquals(tags[0].name, "db");
|
|
115
|
+
// /EVENT should be literal text in the object
|
|
116
|
+
const lits = findAll(ast.object, "Literal");
|
|
117
|
+
// deno-lint-ignore no-explicit-any
|
|
118
|
+
assertEquals(lits.some((l) => (l as any).value.includes("EVENT")), true);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("TagRef in function arguments", () => {
|
|
123
|
+
it("pemit(#weather,msg) → TagRef as first arg", () => {
|
|
124
|
+
const fn = findFirst(mustParse("[pemit(#weather,Hello!)]"), "FunctionCall");
|
|
125
|
+
assertEquals(fn.name, "pemit");
|
|
126
|
+
const tags = findAll(fn.args[0], "TagRef");
|
|
127
|
+
assertEquals(tags.length, 1);
|
|
128
|
+
assertEquals(tags[0].name, "weather");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("get(#globals/SCORE) → TagRef in get() arg", () => {
|
|
132
|
+
const fn = findFirst(mustParse("[get(#globals/SCORE)]"), "FunctionCall");
|
|
133
|
+
assertEquals(fn.name, "get");
|
|
134
|
+
const tags = findAll(fn.args[0], "TagRef");
|
|
135
|
+
assertEquals(tags[0].name, "globals");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("loc(#db) → TagRef in loc() arg", () => {
|
|
139
|
+
const fn = findFirst(mustParse("[loc(#db)]"), "FunctionCall");
|
|
140
|
+
const tags = findAll(fn.args[0], "TagRef");
|
|
141
|
+
assertEquals(tags[0].name, "db");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("set(#weather,RAINY) → TagRef in set() first arg", () => {
|
|
145
|
+
const fn = findFirst(mustParse("[set(#weather,RAINY)]"), "FunctionCall");
|
|
146
|
+
const tags = findAll(fn.args[0], "TagRef");
|
|
147
|
+
assertEquals(tags[0].name, "weather");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("TagRef inside braces", () => {
|
|
152
|
+
it("{@trigger #weather/ATTR} → TagRef inside BracedString", () => {
|
|
153
|
+
const ast = mustParse("{@trigger #weather/ATTR}");
|
|
154
|
+
const tags = findAll(ast, "TagRef");
|
|
155
|
+
assertEquals(tags.length, 1);
|
|
156
|
+
assertEquals(tags[0].name, "weather");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("{pemit #globals hello} → TagRef in braced command", () => {
|
|
160
|
+
const tags = findAll(mustParse("{pemit #globals hello}"), "TagRef");
|
|
161
|
+
assertEquals(tags[0].name, "globals");
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ── @tag command ──────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
describe("@tag command (admin-only global tags)", () => {
|
|
168
|
+
it("@tag/add weather=#4 → AtCommand(tag) with switch 'add'", () => {
|
|
169
|
+
const ast = mustParse("@tag/add weather=#4");
|
|
170
|
+
assertEquals(ast.type, "AtCommand");
|
|
171
|
+
assertEquals(ast.name, "tag");
|
|
172
|
+
assertEquals(ast.switches, ["add"]);
|
|
173
|
+
assertEquals(ast.object.parts[0].value, "weather");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("@tag/remove weather → AtCommand(tag) with switch 'remove', no value", () => {
|
|
177
|
+
const ast = mustParse("@tag/remove weather");
|
|
178
|
+
assertEquals(ast.type, "AtCommand");
|
|
179
|
+
assertEquals(ast.name, "tag");
|
|
180
|
+
assertEquals(ast.switches, ["remove"]);
|
|
181
|
+
assertEquals(ast.object.parts[0].value, "weather");
|
|
182
|
+
assertEquals(ast.value, null);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("@tag/list → AtCommand(tag) with switch 'list', no args", () => {
|
|
186
|
+
const ast = mustParse("@tag/list");
|
|
187
|
+
assertEquals(ast.type, "AtCommand");
|
|
188
|
+
assertEquals(ast.name, "tag");
|
|
189
|
+
assertEquals(ast.switches, ["list"]);
|
|
190
|
+
assertEquals(ast.object, null);
|
|
191
|
+
assertEquals(ast.value, null);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("@tag/list 2 → AtCommand with page number in object position", () => {
|
|
195
|
+
const ast = mustParse("@tag/list 2");
|
|
196
|
+
assertEquals(ast.name, "tag");
|
|
197
|
+
assertEquals(ast.switches, ["list"]);
|
|
198
|
+
// page number lands in object slot
|
|
199
|
+
assertNotEquals(ast.object, null);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("@tag (no switch, no args) → AtCommand(tag), behaves as @tag/list", () => {
|
|
203
|
+
const ast = mustParse("@tag");
|
|
204
|
+
assertEquals(ast.type, "AtCommand");
|
|
205
|
+
assertEquals(ast.name, "tag");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("@tag/add cron=[pmatch(Cron)] → eval block in value position", () => {
|
|
209
|
+
const ast = mustParse("@tag/add cron=[pmatch(Cron)]");
|
|
210
|
+
assertEquals(ast.type, "AtCommand");
|
|
211
|
+
assertEquals(ast.name, "tag");
|
|
212
|
+
assertEquals(ast.switches, ["add"]);
|
|
213
|
+
const fns = findAll(ast.value, "FunctionCall");
|
|
214
|
+
assertEquals(fns[0].name, "pmatch");
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// ── @ltag command ─────────────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
describe("@ltag command (public personal tags)", () => {
|
|
221
|
+
it("@ltag/add mydb=#4 → AtCommand(ltag) with switch 'add'", () => {
|
|
222
|
+
const ast = mustParse("@ltag/add mydb=#4");
|
|
223
|
+
assertEquals(ast.type, "AtCommand");
|
|
224
|
+
assertEquals(ast.name, "ltag");
|
|
225
|
+
assertEquals(ast.switches, ["add"]);
|
|
226
|
+
assertEquals(ast.object.parts[0].value, "mydb");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("@ltag/remove mydb → AtCommand(ltag), switch 'remove'", () => {
|
|
230
|
+
const ast = mustParse("@ltag/remove mydb");
|
|
231
|
+
assertEquals(ast.type, "AtCommand");
|
|
232
|
+
assertEquals(ast.name, "ltag");
|
|
233
|
+
assertEquals(ast.switches, ["remove"]);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("@ltag/list → AtCommand(ltag), switch 'list'", () => {
|
|
237
|
+
const ast = mustParse("@ltag/list");
|
|
238
|
+
assertEquals(ast.type, "AtCommand");
|
|
239
|
+
assertEquals(ast.name, "ltag");
|
|
240
|
+
assertEquals(ast.switches, ["list"]);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// ── tag() / listtags() / tagmatch() functions ─────────────────────────────────
|
|
245
|
+
|
|
246
|
+
describe("tag() function — resolves name to dbref", () => {
|
|
247
|
+
it("[tag(weather)] → FunctionCall(tag)", () => {
|
|
248
|
+
const fn = findFirst(mustParse("[tag(weather)]"), "FunctionCall");
|
|
249
|
+
assertEquals(fn.name, "tag");
|
|
250
|
+
assertEquals(fn.args[0].parts[0].value, "weather");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("@trigger [tag(weather)]/ATTR=hello → FunctionCall in object position", () => {
|
|
254
|
+
const ast = mustParse("@trigger [tag(weather)]/ATTR=hello");
|
|
255
|
+
assertEquals(ast.type, "AtCommand");
|
|
256
|
+
const fn = findFirst(ast.object, "FunctionCall");
|
|
257
|
+
assertEquals(fn.name, "tag");
|
|
258
|
+
assertEquals(fn.args[0].parts[0].value, "weather");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("[set([tag(globals)],RAINY)] → tag() inside set() call", () => {
|
|
262
|
+
const outer = findFirst(mustParse("[set([tag(globals)],RAINY)]"), "FunctionCall");
|
|
263
|
+
assertEquals(outer.name, "set");
|
|
264
|
+
const inner = findFirst(outer.args[0], "FunctionCall");
|
|
265
|
+
assertEquals(inner.name, "tag");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("@pemit %#=[tag(cron)] → tag() call in @pemit value", () => {
|
|
269
|
+
const ast = mustParse("@pemit %#=[tag(cron)]");
|
|
270
|
+
const fn = findFirst(ast.value, "FunctionCall");
|
|
271
|
+
assertEquals(fn.name, "tag");
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe("listtags() function", () => {
|
|
276
|
+
it("[listtags()] → FunctionCall(listtags) zero args", () => {
|
|
277
|
+
const fn = findFirst(mustParse("[listtags()]"), "FunctionCall");
|
|
278
|
+
assertEquals(fn.name, "listtags");
|
|
279
|
+
assertEquals(fn.args.length, 0);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("[listtags(me)] → FunctionCall with object arg", () => {
|
|
283
|
+
const fn = findFirst(mustParse("[listtags(me)]"), "FunctionCall");
|
|
284
|
+
assertEquals(fn.name, "listtags");
|
|
285
|
+
assertEquals(fn.args[0].parts[0].value, "me");
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe("tagmatch() function", () => {
|
|
290
|
+
it("[tagmatch(wea*)] → FunctionCall(tagmatch) with glob pattern", () => {
|
|
291
|
+
const fn = findFirst(mustParse("[tagmatch(wea*)]"), "FunctionCall");
|
|
292
|
+
assertEquals(fn.name, "tagmatch");
|
|
293
|
+
// wildcard '*' in arg is a Wildcard node
|
|
294
|
+
assertEquals(findAll(fn.args[0], "Wildcard").length, 0); // not a pattern context
|
|
295
|
+
// in arg context * is just a literal character
|
|
296
|
+
// deno-lint-ignore no-explicit-any
|
|
297
|
+
const text = fn.args[0].parts.map((p: any) => p.value ?? "").join("");
|
|
298
|
+
assertEquals(text.includes("wea"), true);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("[tagmatch(*)] → FunctionCall(tagmatch) wildcard glob", () => {
|
|
302
|
+
const fn = findFirst(mustParse("[tagmatch(*)]"), "FunctionCall");
|
|
303
|
+
assertEquals(fn.name, "tagmatch");
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// ── Real-world patterns ───────────────────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
describe("Real-world RhostMUSH tag patterns", () => {
|
|
310
|
+
it("@dolist [listtags()]={@tag/remove ##} → removes all tags", () => {
|
|
311
|
+
const ast = mustParse("@dolist [listtags()]={@tag/remove ##}");
|
|
312
|
+
assertEquals(ast.type, "AtCommand");
|
|
313
|
+
assertEquals(ast.name, "dolist");
|
|
314
|
+
const listfn = findFirst(ast.object, "FunctionCall");
|
|
315
|
+
assertEquals(listfn.name, "listtags");
|
|
316
|
+
// Inside the brace there should be an @tag/remove command
|
|
317
|
+
const sv = findAll(ast.value, "SpecialVar");
|
|
318
|
+
assertEquals(sv.some((s) => s.code === "##"), true);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("&CMD_WEATHER Global=$+weather:@pemit %#=[get(#weather/CURR)] → TagRef in attr value", () => {
|
|
322
|
+
const ast = mustParse("&CMD_WEATHER Global=$+weather:@pemit %#=[get(#weather/CURR)]");
|
|
323
|
+
assertEquals(ast.type, "AttributeSet");
|
|
324
|
+
assertEquals(ast.attribute, "CMD_WEATHER");
|
|
325
|
+
// value is a DollarPattern
|
|
326
|
+
assertEquals(ast.value.type, "DollarPattern");
|
|
327
|
+
const tags = findAll(ast.value, "TagRef");
|
|
328
|
+
assertEquals(tags.length, 1);
|
|
329
|
+
assertEquals(tags[0].name, "weather");
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("@tag/add and #tagref in same command list", () => {
|
|
333
|
+
const ast = mustParse("@tag/add db=#4;@pemit %#=[name(#db)]");
|
|
334
|
+
assertEquals(ast.type, "CommandList");
|
|
335
|
+
assertEquals(ast.commands[0].name, "tag");
|
|
336
|
+
const tags = findAll(ast.commands[1], "TagRef");
|
|
337
|
+
assertEquals(tags[0].name, "db");
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("#weather alongside substitution: #weather %N greet", () => {
|
|
341
|
+
const ast = mustParse("#weather %N greet");
|
|
342
|
+
const tags = findAll(ast, "TagRef");
|
|
343
|
+
assertEquals(tags.length, 1);
|
|
344
|
+
assertEquals(tags[0].name, "weather");
|
|
345
|
+
const subs = findAll(ast, "Substitution");
|
|
346
|
+
assertEquals(subs[0].code, "N");
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("multiple TagRefs in one expression", () => {
|
|
350
|
+
const ast = mustParse("@trigger #cron/RUN;@trigger #weather/UPDATE");
|
|
351
|
+
assertEquals(ast.type, "CommandList");
|
|
352
|
+
const tags = findAll(ast, "TagRef");
|
|
353
|
+
assertEquals(tags.length, 2);
|
|
354
|
+
assertEquals(tags[0].name, "cron");
|
|
355
|
+
assertEquals(tags[1].name, "weather");
|
|
356
|
+
});
|
|
357
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// 12 — Source locations: every node carries { start, end } with offset/line/col
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import { assertEquals, assertExists } from "jsr:@std/assert@^1";
|
|
6
|
+
import { describe, it } from "jsr:/@std/testing@^1/bdd";
|
|
7
|
+
import { parse } from "../parser/mod.ts";
|
|
8
|
+
import type { ASTNode, SourceLocation } from "../parser/mod.ts";
|
|
9
|
+
import { findAll } from "./helpers.ts";
|
|
10
|
+
|
|
11
|
+
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
function p(text: string): ASTNode {
|
|
14
|
+
return parse(text, "Start");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function loc(node: ASTNode): SourceLocation {
|
|
18
|
+
assertExists(node.loc, `node type "${node.type}" has no loc`);
|
|
19
|
+
return node.loc!;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ── Basic position smoke-test ──────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
describe("loc — every parsed node has start/end", () => {
|
|
25
|
+
it("root UserCommand spans the whole input", () => {
|
|
26
|
+
const ast = p("hello");
|
|
27
|
+
const l = loc(ast);
|
|
28
|
+
assertEquals(l.start.offset, 0);
|
|
29
|
+
assertEquals(l.end.offset, 5);
|
|
30
|
+
assertEquals(l.start.line, 1);
|
|
31
|
+
assertEquals(l.start.column, 1);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("EvalBlock: offsets cover [ ... ]", () => {
|
|
35
|
+
// [add(1,2)] — offsets 0–10
|
|
36
|
+
const ast = p("[add(1,2)]");
|
|
37
|
+
const ev = findAll(ast, "EvalBlock")[0];
|
|
38
|
+
const l = loc(ev);
|
|
39
|
+
assertEquals(l.start.offset, 0);
|
|
40
|
+
assertEquals(l.end.offset, 10);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("FunctionCall: offsets cover name(...)", () => {
|
|
44
|
+
const ast = p("[add(1,2)]");
|
|
45
|
+
const fn = findAll(ast, "FunctionCall")[0];
|
|
46
|
+
const l = loc(fn);
|
|
47
|
+
// add(1,2) starts at offset 1, ends at offset 9
|
|
48
|
+
assertEquals(l.start.offset, 1);
|
|
49
|
+
assertEquals(l.end.offset, 9);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("Arg: offset covers the argument text", () => {
|
|
53
|
+
const ast = p("[add(1,2)]");
|
|
54
|
+
const args = findAll(ast, "Arg");
|
|
55
|
+
assertEquals(args.length, 2);
|
|
56
|
+
assertEquals(loc(args[0]).start.offset, 5); // "1" at offset 5
|
|
57
|
+
assertEquals(loc(args[0]).end.offset, 6);
|
|
58
|
+
assertEquals(loc(args[1]).start.offset, 7); // "2" at offset 7
|
|
59
|
+
assertEquals(loc(args[1]).end.offset, 8);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("Substitution: loc covers %# exactly", () => {
|
|
63
|
+
const ast = p("%#");
|
|
64
|
+
const subs = findAll(ast, "Substitution");
|
|
65
|
+
assertEquals(subs.length, 1);
|
|
66
|
+
const l = loc(subs[0]);
|
|
67
|
+
assertEquals(l.start.offset, 0);
|
|
68
|
+
assertEquals(l.end.offset, 2); // %# is 2 chars
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("Escape: loc covers \\; exactly", () => {
|
|
72
|
+
const ast = p("\\;");
|
|
73
|
+
const escs = findAll(ast, "Escape");
|
|
74
|
+
const l = loc(escs[0]);
|
|
75
|
+
assertEquals(l.start.offset, 0);
|
|
76
|
+
assertEquals(l.end.offset, 2);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("TagRef: loc covers #tagname", () => {
|
|
80
|
+
const ast = p("#weather");
|
|
81
|
+
const tags = findAll(ast, "TagRef");
|
|
82
|
+
const l = loc(tags[0]);
|
|
83
|
+
assertEquals(l.start.offset, 0);
|
|
84
|
+
assertEquals(l.end.offset, 8); // #weather is 8 chars
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("SpecialVar: loc covers ## exactly", () => {
|
|
88
|
+
const ast = p("##");
|
|
89
|
+
const sv = findAll(ast, "SpecialVar")[0];
|
|
90
|
+
const l = loc(sv);
|
|
91
|
+
assertEquals(l.start.offset, 0);
|
|
92
|
+
assertEquals(l.end.offset, 2);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ── Multi-line positions ────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
describe("loc — line and column numbers", () => {
|
|
99
|
+
it("single-line: column advances correctly", () => {
|
|
100
|
+
// "abc[f()]" — f() starts at column 5 (1-based)
|
|
101
|
+
const ast = p("abc[f()]");
|
|
102
|
+
const fn = findAll(ast, "FunctionCall")[0];
|
|
103
|
+
assertEquals(loc(fn).start.column, 5);
|
|
104
|
+
assertEquals(loc(fn).start.line, 1);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ── Command-level positions ────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
describe("loc — AtCommand and AttributeSet", () => {
|
|
111
|
+
it("AtCommand spans @pemit %#=hi", () => {
|
|
112
|
+
const ast = p("@pemit %#=hi");
|
|
113
|
+
const l = loc(ast);
|
|
114
|
+
assertEquals(l.start.offset, 0);
|
|
115
|
+
assertEquals(l.end.offset, 12);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("AttributeSet loc is present", () => {
|
|
119
|
+
const ast = p("&ATTR me=val");
|
|
120
|
+
assertExists(ast.loc);
|
|
121
|
+
assertEquals(ast.loc!.start.offset, 0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("hidden AttributeSet loc is present", () => {
|
|
125
|
+
const ast = p("&_HIDDEN me=val");
|
|
126
|
+
assertExists(ast.loc);
|
|
127
|
+
assertEquals(ast.hidden, true);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ── Coalesced literals preserve start/end span ────────────────────────────────
|
|
132
|
+
|
|
133
|
+
describe("loc — coalesced Literal spans full merged text", () => {
|
|
134
|
+
it("literal span covers all merged chars", () => {
|
|
135
|
+
// "hello world" — one coalesced Literal
|
|
136
|
+
const ast = p("hello world");
|
|
137
|
+
const lit = findAll(ast, "Literal")[0];
|
|
138
|
+
const l = loc(lit);
|
|
139
|
+
assertEquals(l.start.offset, 0);
|
|
140
|
+
assertEquals(l.end.offset, 11);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("mixed text + sub: Literal before %# has correct span", () => {
|
|
144
|
+
// "Hi %# there" — Literal "Hi ", Substitution, Literal " there"
|
|
145
|
+
const ast = p("Hi %# there");
|
|
146
|
+
const lits = findAll(ast, "Literal");
|
|
147
|
+
assertEquals(loc(lits[0]).start.offset, 0);
|
|
148
|
+
assertEquals(loc(lits[0]).end.offset, 3); // "Hi "
|
|
149
|
+
assertEquals(loc(lits[1]).start.offset, 5); // " there"
|
|
150
|
+
assertEquals(loc(lits[1]).end.offset, 11);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ── Manually constructed nodes have no loc (and that's fine) ──────────────────
|
|
155
|
+
|
|
156
|
+
describe("loc — manually constructed nodes", () => {
|
|
157
|
+
it("node without loc is valid ASTNode", () => {
|
|
158
|
+
const n: ASTNode = { type: "Literal", value: "hi" };
|
|
159
|
+
assertEquals(n.loc, undefined);
|
|
160
|
+
assertEquals(n.type, "Literal");
|
|
161
|
+
});
|
|
162
|
+
});
|