@tikoci/centrs 0.1.9-next.22 → 0.1.9-next.24
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/CHANGELOG.md +106 -0
- package/GLOSSARY.txt +38 -0
- package/README.md +2 -2
- package/commands/api/AGENTS.md +18 -1
- package/commands/api/README.md +10 -6
- package/commands/api/examples.md +1 -1
- package/commands/execute/README.md +26 -6
- package/commands/execute/examples.md +43 -5
- package/commands/explain/README.md +873 -182
- package/commands/explain/examples.md +447 -38
- package/commands/retrieve/README.md +13 -2
- package/commands/retrieve/examples.md +30 -1
- package/commands/transfer/README.md +8 -4
- package/docs/CLI.md +19 -5
- package/docs/CONSTITUTION.md +18 -9
- package/docs/MATRIX.md +51 -26
- package/docs/errors/cmr/caller-policy-not-enforced.md +5 -3
- package/docs/errors/routeros/permission-denied.md +24 -0
- package/docs/errors/transport/edge-auth-failed.md +22 -0
- package/docs/errors/transport/write-outcome-unknown.md +3 -0
- package/docs/errors/usage/invalid-cursor.md +20 -0
- package/docs/errors/validation/unknown-path.md +9 -0
- package/package.json +5 -2
- package/src/api.ts +34 -39
- package/src/cli/explain.ts +206 -47
- package/src/core/envelope.ts +10 -3
- package/src/core/error-catalog.ts +15 -0
- package/src/core/execute-command.ts +53 -0
- package/src/core/fanout.ts +12 -2
- package/src/core/inspect.ts +4 -5
- package/src/core/routeros-errors.ts +25 -0
- package/src/execute-cmr.ts +2 -8
- package/src/execute.ts +48 -20
- package/src/explain/args.ts +181 -15
- package/src/explain/blocks.ts +3 -4
- package/src/explain/brace-slots.ts +5 -0
- package/src/explain/catalog.ts +13 -6
- package/src/explain/defects.ts +1 -0
- package/src/explain/group-faults.ts +1060 -0
- package/src/explain/highlight-projection.ts +10 -8
- package/src/explain/operators.ts +22 -20
- package/src/explain/pathresolve.ts +102 -20
- package/src/explain/quoted-string.ts +41 -6
- package/src/explain/run-tokens.ts +223 -0
- package/src/explain/scope-brace.ts +319 -7
- package/src/explain/segment.ts +177 -4
- package/src/explain/spaced-args.ts +116 -0
- package/src/explain/symbols.ts +163 -7
- package/src/explain/transport.ts +13 -0
- package/src/explain/values.ts +55 -4
- package/src/explain/verbsplit.ts +14 -188
- package/src/explain/write.ts +8 -1
- package/src/explain-live-facets.ts +739 -0
- package/src/explain-live-types.ts +490 -0
- package/src/explain-live.ts +593 -0
- package/src/explain.ts +772 -60
- package/src/index.ts +6 -0
- package/src/offline-gate.ts +30 -9
- package/src/protocols/adapter.ts +88 -10
- package/src/protocols/btest-session.ts +30 -10
- package/src/protocols/mac-telnet-console.ts +16 -0
- package/src/protocols/mac-telnet.ts +38 -10
- package/src/protocols/native-api.ts +13 -12
- package/src/retrieve-follow.ts +2 -1
- package/src/retrieve-sample.ts +2 -1
- package/src/retrieve-wait.ts +10 -6
- package/src/retrieve.ts +77 -27
- package/src/transfer.ts +38 -13
|
@@ -0,0 +1,1060 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device syntax errors inside groups: `( … )` expressions, `{ … }` arrays,
|
|
3
|
+
* the head word of a statement, and the tokens of one (#505, #506, #509, #511,
|
|
4
|
+
* #512, #518).
|
|
5
|
+
*
|
|
6
|
+
* One walk over the comment-masked document, with the same frame roles as the
|
|
7
|
+
* other walkers (`segment.ts` → `maskUncached`): `[` opens a statement
|
|
8
|
+
* context, `(` an expression, and `{` a statement body when its enclosing
|
|
9
|
+
* frame bears statements and either `braceStartsStatements` says so or the
|
|
10
|
+
* statement's directive takes a program in that slot (`:execute {…}`,
|
|
11
|
+
* `:time {…}`, `brace-slots.ts`). Every other `{` is an array. Each group
|
|
12
|
+
* reports at most one fault, because the device stops at its first error and a
|
|
13
|
+
* second report from the same group would describe input it never read.
|
|
14
|
+
*
|
|
15
|
+
* What each rule says was grounded with `:parse` on a stored `/system script`
|
|
16
|
+
* source, on CHR 7.24.5 and 7.23.8, identical on both:
|
|
17
|
+
*
|
|
18
|
+
* - **Expressions (#505).** `()`, `(1 +)`, `(+ 1)`, `(1 + + 2)`, `(!)`,
|
|
19
|
+
* `(1,)` and `(1 any)` are `syntax error`. `(1 2)` (juxtaposition),
|
|
20
|
+
* `(- 1)`, `(1 + -2)`, `(-> 1)` and `(any any 1)` parse: `-`, `!`, `>`, `~`
|
|
21
|
+
* and `any` are prefix operators, and the rest need a left operand. A `;`
|
|
22
|
+
* or a newline directly inside the group is an error too (`(1; 2)`,
|
|
23
|
+
* `(1⏎+ 2)`); a `\`-continued line is not a newline. At operand position
|
|
24
|
+
* `and`, `or`, `in`, `.` and `/` are variable names (`(and 1)` is
|
|
25
|
+
* `( $and 1)`, `(. 1)` is `( $. 1)`), and `1.`, `1/`, `a_b`, `1:2` and
|
|
26
|
+
* `a/b` are single words. `-` never joins a word: `(a-b)` is `(- $a $b)`
|
|
27
|
+
* and `(a-)` an error. A `$name` stops at `.`, `-` and `/`.
|
|
28
|
+
* - **Queries.** Inside `where …` and `find …` the device parses the value
|
|
29
|
+
* after `<name>=` with the property's own parser: `where (comment=+)`,
|
|
30
|
+
* `(comment=a-)`, `(comment=@)` and `(.id=*1)` parse, though each is an
|
|
31
|
+
* expression error elsewhere. So a group in a query gets only the structural
|
|
32
|
+
* checks (empty, `;`, newline), #509's brace rule and the unquoted-byte
|
|
33
|
+
* rule below, never the operand or `@` rules.
|
|
34
|
+
* - **Arrays (#511).** An array that is the whole value of a statement or an
|
|
35
|
+
* argument must have an element: `:local z {}`, `:put {}`, `:return {}`,
|
|
36
|
+
* `[:len {}]` and `in={}` are errors, while `{1;{}}`, `{a={}}`, `({})`,
|
|
37
|
+
* `where … in {}` and a body (`do={}`, `source={}`, `:execute {}`) parse. A
|
|
38
|
+
* `;` with no element before it on its line is an error (`{;1}`, `{1;;2}`,
|
|
39
|
+
* `{1⏎;2}`), and a trailing one is not (`{1;}`, `{1;2;⏎}`). A newline
|
|
40
|
+
* separates elements (`{1⏎2}` is `1;2`). `,` is the list operator, so
|
|
41
|
+
* `{1,}`, `{,1}` and `{1,⏎2}` are errors.
|
|
42
|
+
* - **`@` (#512).** A `@` is an error in the head word of a statement at any
|
|
43
|
+
* level (`admin@x`, `[admin@MikroTik]`, `:put@x`, `/ip@x`), anywhere in an
|
|
44
|
+
* array (`{1;a@b}`, `{a=c@d}`) and in an expression (`(a@b)`). It is a value
|
|
45
|
+
* byte in an argument (`comment=a@b`, `:put a@b`, `[:len a@b]`).
|
|
46
|
+
* - **Named comparisons (#509).** A brace after `<property>=` in a query that
|
|
47
|
+
* `args.ts` cannot see, inside `( … )` or a `[ … ]` command, uses the
|
|
48
|
+
* property's parser and is rejected, as at the top level (#502).
|
|
49
|
+
*
|
|
50
|
+
* - **Tokens (#506, #518).** The same walk reads each statement-level token
|
|
51
|
+
* part by part (a bare word, a string, a `$name`, a group) outside a query:
|
|
52
|
+
* a closed part glued to the next byte, a `$` with no name after it, and a
|
|
53
|
+
* declared name that is not letters and digits. `gluedAt`, `wordFaults` and
|
|
54
|
+
* `nameFault` carry the device rules. One token fault is reported per
|
|
55
|
+
* statement, and the head word is never read.
|
|
56
|
+
* - **Unquoted bytes (#358, #371).** A `?`, a backtick and a non-ASCII byte
|
|
57
|
+
* are never part of a bare word: the device rejects each at its byte in a
|
|
58
|
+
* head word, a name, a `$name`, a value, a query, an array and an
|
|
59
|
+
* expression (`/systemé`, `print?`, `$café`, `url=http://x/a?b`,
|
|
60
|
+
* `where comment=café`, `{a`b}`, `(café)`), but a menu command's argument
|
|
61
|
+
* name at the name's first byte (`addré=1`, `a?b=1`). A second `=` in a
|
|
62
|
+
* menu command's argument is rejected too (`name=a=b`, a bare base64 key
|
|
63
|
+
* ending in `=`). In a query and in an array it is an operator and a key
|
|
64
|
+
* (`where comment=a=b`, `{a=b}` parse), and a query group takes the byte
|
|
65
|
+
* rule but no other (`where (comment=a?b)` and `in {a?b}` are rejected at
|
|
66
|
+
* the `?`, `where (comment=a=b)` parses); a directive's `a=b` is a named
|
|
67
|
+
* argument on 7.24.5 and a syntax error on 7.23.8, so it is left alone. No
|
|
68
|
+
* property parser was found that takes these bytes (`regexp=a?b`,
|
|
69
|
+
* `public-key=…=`, `secret=…==`, `on-event=a=b` are all rejected). Strings
|
|
70
|
+
* and comments carry any byte (`"café"`, `$"café"`, `:local "café"`).
|
|
71
|
+
*
|
|
72
|
+
* Deliberately not here: a `$( … )` group (`:put $({})` is rejected and is
|
|
73
|
+
* read as `({})`), the `@` in a later head word (`/ip address@ print`), and
|
|
74
|
+
* operand rules for array elements other than `,`.
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
import { comparesNamedValue } from "./args.ts";
|
|
78
|
+
import { braceSlotTakesArray, braceSlotTakesProgram } from "./brace-slots.ts";
|
|
79
|
+
import { scanQuotedString } from "./quoted-string.ts";
|
|
80
|
+
import { braceStartsStatements } from "./scope-brace.ts";
|
|
81
|
+
|
|
82
|
+
export type GroupFaultCode =
|
|
83
|
+
| "empty-group"
|
|
84
|
+
| "misplaced-separator"
|
|
85
|
+
| "missing-operand"
|
|
86
|
+
| "invalid-at-sign"
|
|
87
|
+
| "named-value-brace"
|
|
88
|
+
| "glued-token"
|
|
89
|
+
| "invalid-variable"
|
|
90
|
+
| "invalid-name"
|
|
91
|
+
| "unquoted-non-ascii"
|
|
92
|
+
| "unquoted-reserved-byte";
|
|
93
|
+
|
|
94
|
+
export interface GroupFault {
|
|
95
|
+
code: GroupFaultCode;
|
|
96
|
+
/** Analyzed-byte offset, inclusive. */
|
|
97
|
+
start: number;
|
|
98
|
+
/** Analyzed-byte offset, exclusive. */
|
|
99
|
+
end: number;
|
|
100
|
+
/** The operator, separator or opener the fault is about. */
|
|
101
|
+
detail?: string;
|
|
102
|
+
/** The group the fault sits in, for a message that must say which. */
|
|
103
|
+
group?: "(" | "{";
|
|
104
|
+
/**
|
|
105
|
+
* Where an unquoted-byte fault sits when it is not in a value: a menu
|
|
106
|
+
* command's argument name, or a statement's head word (path, command,
|
|
107
|
+
* `$name`). The remedy differs, since neither has a value to quote.
|
|
108
|
+
*/
|
|
109
|
+
site?: "argument-name" | "head-word";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface StatementFrame {
|
|
113
|
+
kind: "statement";
|
|
114
|
+
char: "" | "[" | "{";
|
|
115
|
+
/** The statement has said `where` or `find`, so its groups are queries. */
|
|
116
|
+
query: boolean;
|
|
117
|
+
/** The end of the statement's `where`, the floor for #509. */
|
|
118
|
+
whereEnd: number | null;
|
|
119
|
+
/** The head word without its colon, the key into `brace-slots.ts`. */
|
|
120
|
+
verb: string | null;
|
|
121
|
+
/** The head word is a `$` call, whose arguments no menu reads (`$f a=1`). */
|
|
122
|
+
call: boolean;
|
|
123
|
+
/** Positional operands read after the head. */
|
|
124
|
+
positionals: number;
|
|
125
|
+
/** No token of the current statement read yet. */
|
|
126
|
+
lead: boolean;
|
|
127
|
+
/** The whitespace-delimited token being read, if any. */
|
|
128
|
+
token: StatementToken | null;
|
|
129
|
+
/**
|
|
130
|
+
* The token rules stop for the rest of the statement: one already
|
|
131
|
+
* reported, or a boundary they cannot trust (a string ending in `$"`).
|
|
132
|
+
*/
|
|
133
|
+
settled: boolean;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface StatementToken {
|
|
137
|
+
start: number;
|
|
138
|
+
head: boolean;
|
|
139
|
+
equals: boolean;
|
|
140
|
+
/** Just past the token's last closed part (`"…"`, `$name`, a group), or -1. */
|
|
141
|
+
closedAt: number;
|
|
142
|
+
/** Just past the token's last bare-word run, or -1. */
|
|
143
|
+
wordEnd: number;
|
|
144
|
+
/** The token's first bare `=`, or -1. */
|
|
145
|
+
eqAt: number;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
interface ArrayFrame {
|
|
149
|
+
kind: "array";
|
|
150
|
+
open: number;
|
|
151
|
+
/** What the last significant byte was; `key` is a named element's `=`. */
|
|
152
|
+
last: "open" | "separator" | "comma" | "element" | "key";
|
|
153
|
+
commaAt: number;
|
|
154
|
+
element: boolean;
|
|
155
|
+
/** The current element is `name=value`, which takes a trailing `,`. */
|
|
156
|
+
named: boolean;
|
|
157
|
+
query: boolean;
|
|
158
|
+
done: boolean;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface ExpressionFrame {
|
|
162
|
+
kind: "expression";
|
|
163
|
+
open: number;
|
|
164
|
+
/** Whether the next token must be an operand. */
|
|
165
|
+
wantOperand: boolean;
|
|
166
|
+
empty: boolean;
|
|
167
|
+
/** The operator still waiting for its operand. */
|
|
168
|
+
pending: { start: number; spelling: string } | null;
|
|
169
|
+
query: boolean;
|
|
170
|
+
whereEnd: number | null;
|
|
171
|
+
done: boolean;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* A `{` whose role is not grounded: a brace after a relative menu word
|
|
176
|
+
* (`/ip { route { } }`, a menu body the device accepts empty), or one in a
|
|
177
|
+
* command argument, which `args.ts` already judges. Also every group inside
|
|
178
|
+
* one that has already faulted, since the device never reads past the fault.
|
|
179
|
+
* Only nesting is tracked.
|
|
180
|
+
*/
|
|
181
|
+
interface OpaqueFrame {
|
|
182
|
+
kind: "opaque";
|
|
183
|
+
close: ")" | "]" | "}";
|
|
184
|
+
query: boolean;
|
|
185
|
+
/** Inside a faulted group: every descendant is opaque too. */
|
|
186
|
+
muted: boolean;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
type Frame = StatementFrame | ArrayFrame | ExpressionFrame | OpaqueFrame;
|
|
190
|
+
|
|
191
|
+
/** Prefix operators: the device accepts each with no left operand. */
|
|
192
|
+
const PREFIX = new Set(["-", "!", ">", "~"]);
|
|
193
|
+
|
|
194
|
+
/** Every operator spelling after an operand, longest first. */
|
|
195
|
+
const INFIX_SPELLINGS = [
|
|
196
|
+
"<%%",
|
|
197
|
+
"<<",
|
|
198
|
+
">>",
|
|
199
|
+
"<=",
|
|
200
|
+
">=",
|
|
201
|
+
"!=",
|
|
202
|
+
"&&",
|
|
203
|
+
"||",
|
|
204
|
+
"->",
|
|
205
|
+
"+",
|
|
206
|
+
"-",
|
|
207
|
+
"*",
|
|
208
|
+
"/",
|
|
209
|
+
"%",
|
|
210
|
+
"<",
|
|
211
|
+
">",
|
|
212
|
+
"=",
|
|
213
|
+
"!",
|
|
214
|
+
"~",
|
|
215
|
+
"|",
|
|
216
|
+
"^",
|
|
217
|
+
"&",
|
|
218
|
+
".",
|
|
219
|
+
",",
|
|
220
|
+
];
|
|
221
|
+
const OPERATOR_BYTES = "+-*/%<>=!~|^&.,";
|
|
222
|
+
const WORD_OPERATORS = new Set(["and", "or", "in"]);
|
|
223
|
+
const BARE_DECLARATIONS = new Set(["local", "global"]);
|
|
224
|
+
/** Directives whose first operand is a variable name (`:local a_b` is rejected). */
|
|
225
|
+
const NAME_DIRECTIVES = new Set(["local", "global", "set", "for", "foreach"]);
|
|
226
|
+
/** The ones whose name must also START alphanumeric; `:set (a) 1` parses. */
|
|
227
|
+
const STRICT_NAME_DIRECTIVES = new Set(["local", "global", "foreach"]);
|
|
228
|
+
const ALNUM = /[A-Za-z0-9]/;
|
|
229
|
+
const SUB = "\x1a";
|
|
230
|
+
|
|
231
|
+
/** A byte no bare word holds: `?`, a backtick, a non-ASCII stand-in (#358, #371). */
|
|
232
|
+
function isUnquotedFaultByte(c: string | undefined): boolean {
|
|
233
|
+
return c === "?" || c === "`" || c === SUB;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The first such byte in `[from, end)`, or -1. */
|
|
237
|
+
function unquotedFaultByteAt(masked: string, from: number, end: number) {
|
|
238
|
+
for (let j = from; j < end; j++) if (isUnquotedFaultByte(masked[j])) return j;
|
|
239
|
+
return -1;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** The fault for the byte at `at`; a non-ASCII run is reported whole. */
|
|
243
|
+
function unquotedByteFault(masked: string, at: number): GroupFault {
|
|
244
|
+
if (masked[at] !== SUB)
|
|
245
|
+
return {
|
|
246
|
+
code: "unquoted-reserved-byte",
|
|
247
|
+
start: at,
|
|
248
|
+
end: at + 1,
|
|
249
|
+
detail: masked[at],
|
|
250
|
+
};
|
|
251
|
+
let end = at;
|
|
252
|
+
while (masked[end] === SUB) end++;
|
|
253
|
+
return { code: "unquoted-non-ascii", start: at, end };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function statementFrame(char: StatementFrame["char"]): StatementFrame {
|
|
257
|
+
return {
|
|
258
|
+
kind: "statement",
|
|
259
|
+
char,
|
|
260
|
+
query: false,
|
|
261
|
+
whereEnd: null,
|
|
262
|
+
verb: null,
|
|
263
|
+
call: false,
|
|
264
|
+
positionals: 0,
|
|
265
|
+
lead: true,
|
|
266
|
+
token: null,
|
|
267
|
+
settled: false,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function isSpace(c: string | undefined): boolean {
|
|
272
|
+
return c === " " || c === "\t" || c === "\r";
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function isWordByte(c: string | undefined): boolean {
|
|
276
|
+
return c !== undefined && /[A-Za-z0-9_.:/]/.test(c);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function continuationLength(text: string, at: number): number {
|
|
280
|
+
if (text[at] !== "\\") return 0;
|
|
281
|
+
if (text[at + 1] === "\n") return 2;
|
|
282
|
+
return text[at + 1] === "\r" && text[at + 2] === "\n" ? 3 : 0;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** A statement-frame word ends at whitespace, a separator or a delimiter. */
|
|
286
|
+
function statementWordEnd(text: string, from: number): number {
|
|
287
|
+
let j = from;
|
|
288
|
+
while (j < text.length && !/[\s;"()[\]{}\\]/.test(text[j] as string)) j++;
|
|
289
|
+
return Math.max(j, from + 1);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** The token being read in `frame`, opened at `at` if there is none. */
|
|
293
|
+
function openToken(frame: StatementFrame, at: number) {
|
|
294
|
+
if (frame.token === null)
|
|
295
|
+
frame.token = {
|
|
296
|
+
start: at,
|
|
297
|
+
head: frame.lead,
|
|
298
|
+
equals: false,
|
|
299
|
+
closedAt: -1,
|
|
300
|
+
wordEnd: -1,
|
|
301
|
+
eqAt: -1,
|
|
302
|
+
};
|
|
303
|
+
frame.lead = false;
|
|
304
|
+
return frame.token;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Whether the token rules read this byte of `frame`'s current token. */
|
|
308
|
+
function readsTokens(frame: StatementFrame): frame is StatementFrame & {
|
|
309
|
+
token: StatementToken;
|
|
310
|
+
} {
|
|
311
|
+
return (
|
|
312
|
+
!frame.settled && !frame.query && frame.token !== null && !frame.token.head
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The glued-token fault at `at`, the first byte of a token part (a word, a
|
|
318
|
+
* string, a `$`, a group), or `null`.
|
|
319
|
+
*
|
|
320
|
+
* After a closed part only a separator may follow: `"a"b`, `$v.b`, `(1)"a"`,
|
|
321
|
+
* `[:len a]b`, `{1}b` and `do={…}else={…}` are `expected end of command` at
|
|
322
|
+
* this byte, in a directive and in a command argument alike
|
|
323
|
+
* (`src-address=$v/24`, `dst-port="80",443`). After a bare word the device
|
|
324
|
+
* rejects a `"`, `$`, `(`, `[` or `{` (`a"b"`, `a$v`, `a(1)`), but in a
|
|
325
|
+
* command argument the property's own parser can take the glued part when the
|
|
326
|
+
* word ends in a prefix or a separator (`dst-address-list=!"my list"`,
|
|
327
|
+
* `dst-port=80,"443"` parse on CHR 7.24.5 and 7.23.8). So there the rule
|
|
328
|
+
* fires only after an alphanumeric byte, and never after the `=`.
|
|
329
|
+
*/
|
|
330
|
+
function gluedAt(
|
|
331
|
+
masked: string,
|
|
332
|
+
frame: StatementFrame & { token: StatementToken },
|
|
333
|
+
at: number,
|
|
334
|
+
): GroupFault | null {
|
|
335
|
+
const { token } = frame;
|
|
336
|
+
const before = masked[at - 1];
|
|
337
|
+
const glued =
|
|
338
|
+
token.closedAt === at ||
|
|
339
|
+
(token.wordEnd === at &&
|
|
340
|
+
before !== "=" &&
|
|
341
|
+
(frame.verb !== null || ALNUM.test(before ?? "")));
|
|
342
|
+
if (!glued) return null;
|
|
343
|
+
const c = masked[at];
|
|
344
|
+
frame.settled = true;
|
|
345
|
+
// A stray `\` is `symbols.ts`'s; a non-ASCII byte is its own fault (`"x"é`).
|
|
346
|
+
if (c === "\\") return null;
|
|
347
|
+
if (c === SUB) return unquotedByteFault(masked, at);
|
|
348
|
+
return { code: "glued-token", start: at, end: at + 1 };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Is the `$` at `at` escaped by an odd run of `\` before it? */
|
|
352
|
+
function escapedAt(text: string, at: number): boolean {
|
|
353
|
+
let n = 0;
|
|
354
|
+
for (let j = at - 1; j >= 0 && text[j] === "\\"; j--) n++;
|
|
355
|
+
return n % 2 === 1;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* The faults in one statement-frame word `[at, end)` (a run with no
|
|
360
|
+
* whitespace, separator, quote, bracket or `\` in it), and the token state it
|
|
361
|
+
* leaves for the next part.
|
|
362
|
+
*
|
|
363
|
+
* - **`$` (#506).** Outside a string a `$` takes a name (`[A-Za-z0-9]+`) or
|
|
364
|
+
* `$"…"`; `$_`, `$-`, `$.`, `$` before a space and a bare trailing `$` are
|
|
365
|
+
* `syntax error` at the byte after it. The name is a closed part, so
|
|
366
|
+
* `$a_b`, `$x$x` and `$a->1` are glued tokens. `$(…)`, `$[…]` and `${…}`
|
|
367
|
+
* are rejected too, at the end of the group, and `$$v` after the name;
|
|
368
|
+
* both are left unreported.
|
|
369
|
+
* - **Declared names (#506).** The first operand of `:local`, `:global`,
|
|
370
|
+
* `:set`, `:for` and `:foreach` is a name: `[A-Za-z0-9]+` or a string. Any
|
|
371
|
+
* other byte after the name is `expected end of command` there (`a_b`,
|
|
372
|
+
* `a-b`, `a.b`, `a~b`, `a$v`), and for `:local`, `:global` and `:foreach`
|
|
373
|
+
* so is any first byte that is not alphanumeric (`:local $a`, `:local -a`).
|
|
374
|
+
* `:foreach` takes a comma list (`a,b`, `a,"b"`), and an empty slot in it
|
|
375
|
+
* is rejected (`a,`, `,a`, `a,,b`). A `name=` operand is a named argument
|
|
376
|
+
* and is not read here.
|
|
377
|
+
*/
|
|
378
|
+
function wordFaults(
|
|
379
|
+
masked: string,
|
|
380
|
+
frame: StatementFrame & { token: StatementToken },
|
|
381
|
+
at: number,
|
|
382
|
+
end: number,
|
|
383
|
+
): GroupFault | null {
|
|
384
|
+
const { token } = frame;
|
|
385
|
+
const glued = gluedAt(masked, frame, at);
|
|
386
|
+
if (glued !== null || frame.settled) return glued;
|
|
387
|
+
let j = at;
|
|
388
|
+
if (
|
|
389
|
+
token.start === at &&
|
|
390
|
+
frame.positionals === 0 &&
|
|
391
|
+
frame.verb !== null &&
|
|
392
|
+
NAME_DIRECTIVES.has(frame.verb)
|
|
393
|
+
) {
|
|
394
|
+
const named = nameFault(masked, frame, at, end);
|
|
395
|
+
if (named !== undefined) return named;
|
|
396
|
+
}
|
|
397
|
+
while (j < end) {
|
|
398
|
+
const c = masked[j];
|
|
399
|
+
if (isUnquotedFaultByte(c)) {
|
|
400
|
+
frame.settled = true;
|
|
401
|
+
return (
|
|
402
|
+
argumentNameFault(masked, frame, j, end) ?? unquotedByteFault(masked, j)
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
if (c === "=") {
|
|
406
|
+
if (token.eqAt < 0) token.eqAt = j;
|
|
407
|
+
else if (frame.verb === null && !frame.call) {
|
|
408
|
+
frame.settled = true;
|
|
409
|
+
return unquotedByteFault(masked, j);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (c !== "$") {
|
|
413
|
+
j++;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (j > at) {
|
|
417
|
+
token.wordEnd = j;
|
|
418
|
+
const fault = gluedAt(masked, frame, j);
|
|
419
|
+
if (fault !== null || frame.settled) return fault;
|
|
420
|
+
}
|
|
421
|
+
const next = masked[j + 1];
|
|
422
|
+
if (next !== undefined && ALNUM.test(next)) {
|
|
423
|
+
let k = j + 1;
|
|
424
|
+
while (k < end && ALNUM.test(masked[k] as string)) k++;
|
|
425
|
+
token.closedAt = k;
|
|
426
|
+
token.wordEnd = -1;
|
|
427
|
+
if (k < end) return gluedAt(masked, frame, k);
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
if (next === '"') {
|
|
431
|
+
// `$"name"`: the string branch reads the name, not glued to the `$`.
|
|
432
|
+
token.closedAt = -1;
|
|
433
|
+
token.wordEnd = -1;
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
if (next === SUB) {
|
|
437
|
+
frame.settled = true;
|
|
438
|
+
return unquotedByteFault(masked, j + 1);
|
|
439
|
+
}
|
|
440
|
+
// `$$v` is rejected after the name, not at the second `$`.
|
|
441
|
+
if (next === "(" || next === "[" || next === "{" || next === "$") {
|
|
442
|
+
frame.settled = true;
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
frame.settled = true;
|
|
446
|
+
return {
|
|
447
|
+
code: "invalid-variable",
|
|
448
|
+
start: j,
|
|
449
|
+
end: Math.min(j + 2, masked.length),
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
token.wordEnd = end;
|
|
453
|
+
token.closedAt = -1;
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* A bad byte at `at` inside a menu command's argument name: the device
|
|
459
|
+
* reports it at the name's first byte (`/ip/address/add addré=1` and
|
|
460
|
+
* `a?b=1` at the `a`), and the span is the name. A directive (`:put café=1`)
|
|
461
|
+
* and a `$` call (`$f café=1`) report at the byte, so this is `null` there.
|
|
462
|
+
*/
|
|
463
|
+
function argumentNameFault(
|
|
464
|
+
masked: string,
|
|
465
|
+
frame: StatementFrame & { token: StatementToken },
|
|
466
|
+
at: number,
|
|
467
|
+
end: number,
|
|
468
|
+
): GroupFault | null {
|
|
469
|
+
const { token } = frame;
|
|
470
|
+
if (frame.verb !== null || frame.call || token.eqAt >= 0) return null;
|
|
471
|
+
const eq = masked.indexOf("=", at);
|
|
472
|
+
if (eq < 0 || eq >= end) return null;
|
|
473
|
+
const fault = unquotedByteFault(masked, at);
|
|
474
|
+
return { ...fault, start: token.start, end: eq, site: "argument-name" };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* The declared-name fault in the word at `at`, `null` when the name is
|
|
479
|
+
* well formed, or `undefined` when the word is not a bare name at all (a
|
|
480
|
+
* `name=` argument, or a `:set`/`:for` operand that is not one).
|
|
481
|
+
*/
|
|
482
|
+
function nameFault(
|
|
483
|
+
masked: string,
|
|
484
|
+
frame: StatementFrame & { token: StatementToken },
|
|
485
|
+
at: number,
|
|
486
|
+
end: number,
|
|
487
|
+
): GroupFault | null | undefined {
|
|
488
|
+
const verb = frame.verb as string;
|
|
489
|
+
const fault = (start: number): GroupFault => {
|
|
490
|
+
frame.settled = true;
|
|
491
|
+
return {
|
|
492
|
+
code: "invalid-name",
|
|
493
|
+
start: Math.min(start, masked.length - 1),
|
|
494
|
+
end: Math.min(start, masked.length - 1) + 1,
|
|
495
|
+
};
|
|
496
|
+
};
|
|
497
|
+
if (!ALNUM.test(masked[at] as string))
|
|
498
|
+
return STRICT_NAME_DIRECTIVES.has(verb) ? fault(at) : undefined;
|
|
499
|
+
let k = at;
|
|
500
|
+
for (;;) {
|
|
501
|
+
while (k < end && ALNUM.test(masked[k] as string)) k++;
|
|
502
|
+
const next = masked[k];
|
|
503
|
+
if (k === end) {
|
|
504
|
+
frame.token.wordEnd = end;
|
|
505
|
+
frame.token.closedAt = -1;
|
|
506
|
+
// `a,"b"`: a string may fill a slot, and the rules after it are the
|
|
507
|
+
// list's, not the glued-token rule's.
|
|
508
|
+
if (verb === "foreach" && masked[k - 1] === ",") frame.settled = true;
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
if (next === "=") return undefined;
|
|
512
|
+
if (next === "," && verb === "foreach") {
|
|
513
|
+
const after = masked[k + 1];
|
|
514
|
+
if (after !== undefined && ALNUM.test(after)) {
|
|
515
|
+
k++;
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
if (after === '"' && k + 1 === end) {
|
|
519
|
+
k++;
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
return fault(k + 1);
|
|
523
|
+
}
|
|
524
|
+
return fault(k);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function closeToken(frame: StatementFrame) {
|
|
529
|
+
if (frame.token !== null && !frame.token.head && !frame.token.equals)
|
|
530
|
+
frame.positionals++;
|
|
531
|
+
frame.token = null;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* What the `{` at `at` is in a root directive's slot, by the device-swept
|
|
536
|
+
* `(verb, slot)` table: a program (`:execute {…}`, `:time {…}`, which
|
|
537
|
+
* `braceStartsStatements` does not know), an array (`:local z {…}`,
|
|
538
|
+
* `in={…}`), or unknown.
|
|
539
|
+
*/
|
|
540
|
+
function directiveSlotRole(
|
|
541
|
+
masked: string,
|
|
542
|
+
at: number,
|
|
543
|
+
frame: StatementFrame,
|
|
544
|
+
): "program" | "array" | null {
|
|
545
|
+
if (frame.verb === null) return null;
|
|
546
|
+
const token = frame.token;
|
|
547
|
+
let slot: string;
|
|
548
|
+
if (token === null) slot = `#${frame.positionals}`;
|
|
549
|
+
else if (!token.head && masked[at - 1] === "=")
|
|
550
|
+
slot = masked.slice(token.start, at - 1).toLowerCase();
|
|
551
|
+
else return null;
|
|
552
|
+
if (braceSlotTakesProgram(frame.verb, slot)) return "program";
|
|
553
|
+
return braceSlotTakesArray(frame.verb, slot) ? "array" : null;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const CLOSER = { "(": ")", "[": "]", "{": "}" } as const;
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Find every group fault in an analyzed document. `masked` is its comment
|
|
560
|
+
* mask; `text` is read only to tell a masked comment line from spaces. The
|
|
561
|
+
* walk starts at `from`, past a leading BOM, which is a positional fact and
|
|
562
|
+
* not a byte of the first word.
|
|
563
|
+
*/
|
|
564
|
+
export function findGroupFaults(
|
|
565
|
+
text: string,
|
|
566
|
+
masked: string,
|
|
567
|
+
from = 0,
|
|
568
|
+
): GroupFault[] {
|
|
569
|
+
const faults: GroupFault[] = [];
|
|
570
|
+
const frames: Frame[] = [statementFrame("")];
|
|
571
|
+
const top = (): Frame => frames[frames.length - 1] as Frame;
|
|
572
|
+
const report = (fault: GroupFault, frame: ArrayFrame | ExpressionFrame) => {
|
|
573
|
+
faults.push(fault);
|
|
574
|
+
frame.done = true;
|
|
575
|
+
};
|
|
576
|
+
// A completed operand (a word, string, `$name` or closed group).
|
|
577
|
+
const operand = () => {
|
|
578
|
+
const frame = top();
|
|
579
|
+
if (frame.kind === "array") {
|
|
580
|
+
frame.element = true;
|
|
581
|
+
frame.last = "element";
|
|
582
|
+
} else if (frame.kind === "expression") {
|
|
583
|
+
frame.empty = false;
|
|
584
|
+
frame.wantOperand = false;
|
|
585
|
+
frame.pending = null;
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
// H5, as `segment.ts` → `maskUncached` reads it: a `\` continuation runs on
|
|
590
|
+
// across blank lines and line-leading comment lines, and a blank line after
|
|
591
|
+
// a comment line ends it. CHR 7.24.5 and 7.23.8 join `(1 +\⏎# c⏎2)` and
|
|
592
|
+
// `(1 +\⏎⏎2)`, and reject `(1 +\⏎# c⏎⏎2)` at the blank line.
|
|
593
|
+
let cont: "none" | "escape" | "comment" = "none";
|
|
594
|
+
let contLineStart = false;
|
|
595
|
+
let i = from;
|
|
596
|
+
while (i < masked.length) {
|
|
597
|
+
const c = masked[i] as string;
|
|
598
|
+
const frame = top();
|
|
599
|
+
if (cont !== "none") {
|
|
600
|
+
if (c === "\n" && cont === "escape") {
|
|
601
|
+
contLineStart = true;
|
|
602
|
+
i++;
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
if (text[i] === "#" && contLineStart) {
|
|
606
|
+
const newline = masked.indexOf("\n", i);
|
|
607
|
+
cont = "comment";
|
|
608
|
+
contLineStart = true;
|
|
609
|
+
i = newline < 0 ? masked.length : newline + 1;
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
if (isSpace(c)) contLineStart = false;
|
|
613
|
+
else cont = "none";
|
|
614
|
+
}
|
|
615
|
+
const continued = continuationLength(masked, i);
|
|
616
|
+
if (continued > 0 || isSpace(c)) {
|
|
617
|
+
if (continued > 0) {
|
|
618
|
+
cont = "escape";
|
|
619
|
+
contLineStart = true;
|
|
620
|
+
}
|
|
621
|
+
if (frame.kind === "statement") closeToken(frame);
|
|
622
|
+
i += Math.max(continued, 1);
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
if (c === '"') {
|
|
626
|
+
const end = scanQuotedString(masked, i).end;
|
|
627
|
+
if (frame.kind === "statement") {
|
|
628
|
+
openToken(frame, i);
|
|
629
|
+
if (readsTokens(frame)) {
|
|
630
|
+
const glued = gluedAt(masked, frame, i);
|
|
631
|
+
if (glued !== null) faults.push(glued);
|
|
632
|
+
frame.token.closedAt = end;
|
|
633
|
+
frame.token.wordEnd = -1;
|
|
634
|
+
// `:foreach "a",b in=…` parses: a string fills a counter slot.
|
|
635
|
+
if (
|
|
636
|
+
frame.verb === "foreach" &&
|
|
637
|
+
frame.positionals === 0 &&
|
|
638
|
+
frame.token.start === i
|
|
639
|
+
)
|
|
640
|
+
frame.settled = true;
|
|
641
|
+
// `"a$"v""` is ONE string on the device, which reads `$"` as a
|
|
642
|
+
// quoted name; every scanner here ends it at the second quote.
|
|
643
|
+
if (masked[end - 2] === "$" && !escapedAt(masked, end - 2))
|
|
644
|
+
frame.settled = true;
|
|
645
|
+
}
|
|
646
|
+
} else operand();
|
|
647
|
+
i = end;
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
if (c === "(" || c === "[" || c === "{") {
|
|
651
|
+
if (frame.kind === "statement") {
|
|
652
|
+
if (readsTokens(frame)) {
|
|
653
|
+
const glued = gluedAt(masked, frame, i);
|
|
654
|
+
if (glued !== null) faults.push(glued);
|
|
655
|
+
} else if (
|
|
656
|
+
frame.token === null &&
|
|
657
|
+
!frame.lead &&
|
|
658
|
+
!frame.settled &&
|
|
659
|
+
frame.positionals === 0 &&
|
|
660
|
+
frame.verb !== null &&
|
|
661
|
+
STRICT_NAME_DIRECTIVES.has(frame.verb)
|
|
662
|
+
) {
|
|
663
|
+
// `:local (a)`, `:local [x]`, `:local {a}`: a name never starts
|
|
664
|
+
// with a group (`syntax error` at it).
|
|
665
|
+
frame.settled = true;
|
|
666
|
+
faults.push({ code: "invalid-name", start: i, end: i + 1 });
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
const query = frame.query;
|
|
670
|
+
const whereEnd =
|
|
671
|
+
frame.kind === "statement" || frame.kind === "expression"
|
|
672
|
+
? frame.whereEnd
|
|
673
|
+
: null;
|
|
674
|
+
if (
|
|
675
|
+
((frame.kind === "array" || frame.kind === "expression") &&
|
|
676
|
+
frame.done) ||
|
|
677
|
+
(frame.kind === "opaque" && frame.muted)
|
|
678
|
+
) {
|
|
679
|
+
frames.push({ kind: "opaque", close: CLOSER[c], query, muted: true });
|
|
680
|
+
i++;
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
// #509: the top level and scope bodies are `args.ts`'s; a bracket
|
|
684
|
+
// command and a group are only visible here.
|
|
685
|
+
const floor =
|
|
686
|
+
frame.kind === "expression" ||
|
|
687
|
+
(frame.kind === "statement" && frame.char === "[")
|
|
688
|
+
? whereEnd
|
|
689
|
+
: null;
|
|
690
|
+
// A `where` named comparison never takes a script body through an
|
|
691
|
+
// unproven name — `[find where comment={ :beep }]` is `expected
|
|
692
|
+
// comment value` on CHR 7.25 — so the brace reads as a VALUE for
|
|
693
|
+
// the role below and the fault stands (#554). Proven slots still
|
|
694
|
+
// read as bodies; the gate is only the unproven disjunct.
|
|
695
|
+
const inWhereComparison =
|
|
696
|
+
c === "{" && floor !== null && comparesNamedValue(masked, i, floor);
|
|
697
|
+
const role =
|
|
698
|
+
c === "{" && frame.kind === "statement"
|
|
699
|
+
? braceStartsStatements(
|
|
700
|
+
masked,
|
|
701
|
+
i,
|
|
702
|
+
0,
|
|
703
|
+
inWhereComparison ? "value" : "body",
|
|
704
|
+
)
|
|
705
|
+
? "program"
|
|
706
|
+
: directiveSlotRole(masked, i, frame)
|
|
707
|
+
: null;
|
|
708
|
+
// A body (`do={`) is never a comparison, though `where` is still open:
|
|
709
|
+
// corpus `eworm/hotspot-to-wpa.template.rsc` interleaves two `:if`s.
|
|
710
|
+
if (c === "{" && role !== "program" && inWhereComparison) {
|
|
711
|
+
// The device stops at this brace, so neither its contents nor the
|
|
712
|
+
// rest of the enclosing group are read.
|
|
713
|
+
faults.push({ code: "named-value-brace", start: i, end: i + 1 });
|
|
714
|
+
if (frame.kind === "expression") frame.done = true;
|
|
715
|
+
frames.push({ kind: "opaque", close: "}", query, muted: true });
|
|
716
|
+
if (frame.kind === "statement") openToken(frame, i);
|
|
717
|
+
i++;
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
if (c === "[") frames.push(statementFrame("["));
|
|
721
|
+
else if (c === "(")
|
|
722
|
+
frames.push({
|
|
723
|
+
kind: "expression",
|
|
724
|
+
open: i,
|
|
725
|
+
wantOperand: true,
|
|
726
|
+
empty: true,
|
|
727
|
+
pending: null,
|
|
728
|
+
query,
|
|
729
|
+
whereEnd,
|
|
730
|
+
done: false,
|
|
731
|
+
});
|
|
732
|
+
else if (role === "program") frames.push(statementFrame("{"));
|
|
733
|
+
else if (
|
|
734
|
+
frame.kind === "opaque" ||
|
|
735
|
+
(frame.kind === "statement" && role !== "array")
|
|
736
|
+
)
|
|
737
|
+
frames.push({ kind: "opaque", close: "}", query, muted: false });
|
|
738
|
+
else
|
|
739
|
+
frames.push({
|
|
740
|
+
kind: "array",
|
|
741
|
+
open: i,
|
|
742
|
+
last: "open",
|
|
743
|
+
commaAt: -1,
|
|
744
|
+
element: false,
|
|
745
|
+
named: false,
|
|
746
|
+
query,
|
|
747
|
+
done: false,
|
|
748
|
+
});
|
|
749
|
+
// After the slot was read: the brace starts or continues a token.
|
|
750
|
+
if (frame.kind === "statement") openToken(frame, i);
|
|
751
|
+
i++;
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
if (c === ")" || c === "]" || c === "}") {
|
|
755
|
+
const matches =
|
|
756
|
+
frame.kind === "expression"
|
|
757
|
+
? c === ")"
|
|
758
|
+
: frame.kind === "array"
|
|
759
|
+
? c === "}"
|
|
760
|
+
: frame.kind === "opaque"
|
|
761
|
+
? c === frame.close
|
|
762
|
+
: frame.char === (c === "]" ? "[" : c === "}" ? "{" : "");
|
|
763
|
+
if (!matches || frames.length === 1) {
|
|
764
|
+
i++;
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
frames.pop();
|
|
768
|
+
const parent = top();
|
|
769
|
+
if (parent.kind === "statement" && parent.token !== null) {
|
|
770
|
+
parent.token.closedAt = i + 1;
|
|
771
|
+
parent.token.wordEnd = -1;
|
|
772
|
+
}
|
|
773
|
+
if (frame.kind === "expression" && !frame.done && frame.wantOperand) {
|
|
774
|
+
if (frame.empty)
|
|
775
|
+
faults.push({
|
|
776
|
+
code: "empty-group",
|
|
777
|
+
start: frame.open,
|
|
778
|
+
end: i + 1,
|
|
779
|
+
detail: "(",
|
|
780
|
+
});
|
|
781
|
+
else if (frame.pending !== null)
|
|
782
|
+
faults.push({
|
|
783
|
+
code: "missing-operand",
|
|
784
|
+
start: frame.pending.start,
|
|
785
|
+
end: frame.pending.start + frame.pending.spelling.length,
|
|
786
|
+
detail: frame.pending.spelling,
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
if (frame.kind === "array" && !frame.done) {
|
|
790
|
+
if (frame.last === "comma" && !frame.named)
|
|
791
|
+
faults.push({
|
|
792
|
+
code: "missing-operand",
|
|
793
|
+
start: frame.commaAt,
|
|
794
|
+
end: frame.commaAt + 1,
|
|
795
|
+
detail: ",",
|
|
796
|
+
});
|
|
797
|
+
else if (!frame.element && parent.kind === "statement")
|
|
798
|
+
faults.push({
|
|
799
|
+
code: "empty-group",
|
|
800
|
+
start: frame.open,
|
|
801
|
+
end: i + 1,
|
|
802
|
+
detail: "{",
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
operand();
|
|
806
|
+
i++;
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (frame.kind === "statement") {
|
|
811
|
+
if (c === ";" || c === "\n") {
|
|
812
|
+
frames[frames.length - 1] = statementFrame(frame.char);
|
|
813
|
+
i++;
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
const end = statementWordEnd(masked, i);
|
|
817
|
+
const word = masked.slice(i, end);
|
|
818
|
+
const token = openToken(frame, i);
|
|
819
|
+
if (token.head && token.start === i) {
|
|
820
|
+
// The head word runs to its first `=` too: `a=b@c` is no head.
|
|
821
|
+
const cut = word.indexOf("=");
|
|
822
|
+
const at = (cut < 0 ? word : word.slice(0, cut)).indexOf("@");
|
|
823
|
+
if (at >= 0)
|
|
824
|
+
faults.push({
|
|
825
|
+
code: "invalid-at-sign",
|
|
826
|
+
start: i + at,
|
|
827
|
+
end: i + at + 1,
|
|
828
|
+
});
|
|
829
|
+
// A `:` directive, or a bare declaration: `local where (comment={})`
|
|
830
|
+
// is `/localname=$where` at the root on CHR 7.24.5 and 7.23.8 (in a
|
|
831
|
+
// menu it is `bad command name local`). A bare `set` or `find` may be
|
|
832
|
+
// a menu command, so it stays unread.
|
|
833
|
+
frame.verb = word.startsWith(":")
|
|
834
|
+
? word.slice(1)
|
|
835
|
+
: BARE_DECLARATIONS.has(word)
|
|
836
|
+
? word
|
|
837
|
+
: null;
|
|
838
|
+
frame.call = word.startsWith("$");
|
|
839
|
+
// `[/ip/address/find comment=a=b]` is a query, as `[find …]` is.
|
|
840
|
+
if (frame.verb === null && /(^|\/)find$/i.test(word))
|
|
841
|
+
frame.query = true;
|
|
842
|
+
const bad = unquotedFaultByteAt(masked, i, end);
|
|
843
|
+
if (bad >= 0 && (at < 0 || i + at > bad)) {
|
|
844
|
+
frame.settled = true;
|
|
845
|
+
faults.push({ ...unquotedByteFault(masked, bad), site: "head-word" });
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
if (readsTokens(frame)) {
|
|
849
|
+
const fault = wordFaults(masked, frame, i, end);
|
|
850
|
+
if (fault !== null) faults.push(fault);
|
|
851
|
+
} else if (frame.query && !frame.settled && !token.head) {
|
|
852
|
+
// A query keeps `=` as an operator, but not these bytes.
|
|
853
|
+
const bad = unquotedFaultByteAt(masked, i, end);
|
|
854
|
+
if (bad >= 0) {
|
|
855
|
+
frame.settled = true;
|
|
856
|
+
faults.push(unquotedByteFault(masked, bad));
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
if (word.includes("=")) token.equals = true;
|
|
860
|
+
const lower = word.toLowerCase();
|
|
861
|
+
// A directive's operand is a name, never a query: `:local where
|
|
862
|
+
// (comment={})` parses, and `:local where (1 +)` is a syntax error.
|
|
863
|
+
if (
|
|
864
|
+
frame.verb === null &&
|
|
865
|
+
token.start === i &&
|
|
866
|
+
(lower === "where" || lower === "find")
|
|
867
|
+
) {
|
|
868
|
+
frame.query = true;
|
|
869
|
+
if (lower === "where") frame.whereEnd = end;
|
|
870
|
+
}
|
|
871
|
+
i = end;
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (frame.kind === "opaque") {
|
|
876
|
+
// A query's `in {…}` is read as opaque, so `where … in {}` keeps
|
|
877
|
+
// parsing; its bytes are still rejected (`in {a?b}` at the `?`).
|
|
878
|
+
if (frame.query && !frame.muted && isUnquotedFaultByte(c)) {
|
|
879
|
+
faults.push(unquotedByteFault(masked, i));
|
|
880
|
+
frame.muted = true;
|
|
881
|
+
}
|
|
882
|
+
i++;
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (frame.kind === "array") {
|
|
887
|
+
if (frame.done) {
|
|
888
|
+
i++;
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
if (c === ";" || c === "\n") {
|
|
892
|
+
// A named element takes a trailing `,`: `{a=1,}` is `value=a` and
|
|
893
|
+
// `{a=1,;b=2}` is `value=a;b=2` on CHR 7.24.5 and 7.23.8, while
|
|
894
|
+
// `{1,}`, `{a=,1}` and `{a=1,,2}` are syntax errors.
|
|
895
|
+
if (frame.last === "comma" && !frame.named)
|
|
896
|
+
report(
|
|
897
|
+
{
|
|
898
|
+
code: "missing-operand",
|
|
899
|
+
start: frame.commaAt,
|
|
900
|
+
end: frame.commaAt + 1,
|
|
901
|
+
detail: ",",
|
|
902
|
+
},
|
|
903
|
+
frame,
|
|
904
|
+
);
|
|
905
|
+
else if (
|
|
906
|
+
c === ";" &&
|
|
907
|
+
(frame.last === "open" || frame.last === "separator")
|
|
908
|
+
)
|
|
909
|
+
report(
|
|
910
|
+
{
|
|
911
|
+
code: "misplaced-separator",
|
|
912
|
+
start: i,
|
|
913
|
+
end: i + 1,
|
|
914
|
+
detail: ";",
|
|
915
|
+
group: "{",
|
|
916
|
+
},
|
|
917
|
+
frame,
|
|
918
|
+
);
|
|
919
|
+
frame.last = "separator";
|
|
920
|
+
frame.named = false;
|
|
921
|
+
i++;
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
if (c === "=" && frame.last === "element" && !frame.named) {
|
|
925
|
+
frame.named = true;
|
|
926
|
+
frame.last = "key";
|
|
927
|
+
i++;
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
if (c === ",") {
|
|
931
|
+
if (frame.last !== "element")
|
|
932
|
+
report(
|
|
933
|
+
{ code: "missing-operand", start: i, end: i + 1, detail: "," },
|
|
934
|
+
frame,
|
|
935
|
+
);
|
|
936
|
+
frame.last = "comma";
|
|
937
|
+
frame.commaAt = i;
|
|
938
|
+
i++;
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
if (c === "@" && !frame.query) {
|
|
942
|
+
report({ code: "invalid-at-sign", start: i, end: i + 1 }, frame);
|
|
943
|
+
i++;
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
// Unlike `@`, these bytes are rejected in a query too (`in {a?b}`).
|
|
947
|
+
if (isUnquotedFaultByte(c)) {
|
|
948
|
+
report(unquotedByteFault(masked, i), frame);
|
|
949
|
+
i++;
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
frame.element = true;
|
|
953
|
+
frame.last = "element";
|
|
954
|
+
i++;
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// An expression group.
|
|
959
|
+
if (frame.done) {
|
|
960
|
+
i++;
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
if (c === ";" || c === "\n") {
|
|
964
|
+
report(
|
|
965
|
+
{
|
|
966
|
+
code: "misplaced-separator",
|
|
967
|
+
start: i,
|
|
968
|
+
end: i + 1,
|
|
969
|
+
detail: c,
|
|
970
|
+
group: "(",
|
|
971
|
+
},
|
|
972
|
+
frame,
|
|
973
|
+
);
|
|
974
|
+
i++;
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
// A query group keeps its operators, but not these bytes:
|
|
978
|
+
// `where (comment=a?b)` is a `syntax error` at the `?`.
|
|
979
|
+
if (isUnquotedFaultByte(c)) {
|
|
980
|
+
report(unquotedByteFault(masked, i), frame);
|
|
981
|
+
i++;
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
if (frame.query) {
|
|
985
|
+
// Structural and byte checks only (module header): any other byte is
|
|
986
|
+
// content.
|
|
987
|
+
frame.empty = false;
|
|
988
|
+
frame.wantOperand = false;
|
|
989
|
+
i++;
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
if (c === "@") {
|
|
993
|
+
report({ code: "invalid-at-sign", start: i, end: i + 1 }, frame);
|
|
994
|
+
i++;
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
if (c === "$") {
|
|
998
|
+
const next = masked[i + 1];
|
|
999
|
+
if (next !== undefined && /[A-Za-z0-9_]/.test(next)) {
|
|
1000
|
+
let j = i + 1;
|
|
1001
|
+
while (j < masked.length && /[A-Za-z0-9_]/.test(masked[j] as string))
|
|
1002
|
+
j++;
|
|
1003
|
+
operand();
|
|
1004
|
+
i = j;
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
// `$"name"` and `$(…)`/`$[…]`/`${…}`: the operand is what follows.
|
|
1008
|
+
if (next === '"' || next === "(" || next === "[" || next === "{") {
|
|
1009
|
+
i++;
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
frame.done = true;
|
|
1013
|
+
i++;
|
|
1014
|
+
continue;
|
|
1015
|
+
}
|
|
1016
|
+
// `.`, `/` and `:` start a word only where an operand is due: `(. 1)` is
|
|
1017
|
+
// `( $. 1)` and `(::1)` an address, while `(1 .2)` is `(. 1 2)`.
|
|
1018
|
+
if (isWordByte(c) && (/[A-Za-z0-9_]/.test(c) || frame.wantOperand)) {
|
|
1019
|
+
let j = i;
|
|
1020
|
+
while (j < masked.length && isWordByte(masked[j])) j++;
|
|
1021
|
+
const word = masked.slice(i, j);
|
|
1022
|
+
if (word === "any" || (!frame.wantOperand && WORD_OPERATORS.has(word))) {
|
|
1023
|
+
frame.empty = false;
|
|
1024
|
+
frame.wantOperand = true;
|
|
1025
|
+
frame.pending = { start: i, spelling: word };
|
|
1026
|
+
} else operand();
|
|
1027
|
+
i = j;
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
if (OPERATOR_BYTES.includes(c)) {
|
|
1031
|
+
const spelling =
|
|
1032
|
+
frame.wantOperand && PREFIX.has(c)
|
|
1033
|
+
? c
|
|
1034
|
+
: (INFIX_SPELLINGS.find((s) => masked.startsWith(s, i)) ?? c);
|
|
1035
|
+
if (frame.wantOperand && !PREFIX.has(c)) {
|
|
1036
|
+
report(
|
|
1037
|
+
{
|
|
1038
|
+
code: "missing-operand",
|
|
1039
|
+
start: i,
|
|
1040
|
+
end: i + spelling.length,
|
|
1041
|
+
detail: spelling,
|
|
1042
|
+
},
|
|
1043
|
+
frame,
|
|
1044
|
+
);
|
|
1045
|
+
i += spelling.length;
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
frame.empty = false;
|
|
1049
|
+
frame.wantOperand = true;
|
|
1050
|
+
frame.pending = { start: i, spelling };
|
|
1051
|
+
i += spelling.length;
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
1054
|
+
// `#`, `'`, a stray `\`: another rule's byte, and nothing here can say
|
|
1055
|
+
// what the group means after it.
|
|
1056
|
+
frame.done = true;
|
|
1057
|
+
i++;
|
|
1058
|
+
}
|
|
1059
|
+
return faults;
|
|
1060
|
+
}
|