pi-supernova 0.10.2 → 0.11.0
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 +365 -90
- package/docs/CHANGELOG.md +89 -5
- package/docs/TOKEN_COSTS.md +149 -324
- package/index.js +40 -11
- package/package.json +4 -3
- package/src/adapters/bash.js +40 -10
- package/src/adapters/edit.js +77 -23
- package/src/adapters/errors.js +4 -0
- package/src/adapters/index.js +1 -0
- package/src/adapters/list.js +6 -3
- package/src/adapters/read-focus.js +6 -0
- package/src/adapters/read-image.js +17 -9
- package/src/adapters/read-indexed.js +72 -0
- package/src/adapters/read-text.js +19 -2
- package/src/adapters/read.js +57 -13
- package/src/adapters/refs.js +17 -7
- package/src/adapters/write.js +14 -21
- package/src/bridge/host-bridge.js +152 -39
- package/src/bridge/invoke.js +3 -1
- package/src/bridge/tool-registry.js +132 -16
- package/src/context/evidence-graph.js +1 -0
- package/src/context/evidence-rank.js +1 -0
- package/src/context/evidence.js +4 -2
- package/src/context/fuzzy.js +31 -8
- package/src/context/outline.js +33 -9
- package/src/context/query.js +7 -2
- package/src/context/repo-index.js +8 -3
- package/src/context/search-files.js +20 -0
- package/src/context/search.js +25 -10
- package/src/context/snap-search.js +28 -6
- package/src/context/snap.js +8 -3
- package/src/context/source-entry.js +64 -28
- package/src/context/spans.js +1 -0
- package/src/contract/bash.js +16 -7
- package/src/contract/edit.js +26 -7
- package/src/contract/program.js +2 -1
- package/src/contract/read.js +46 -46
- package/src/fs/background.js +10 -2
- package/src/fs/check.js +844 -46
- package/src/fs/commit.js +89 -15
- package/src/fs/diff.js +12 -9
- package/src/fs/directory.js +14 -4
- package/src/fs/file-io.js +28 -7
- package/src/fs/glob.js +93 -11
- package/src/fs/json-read.js +3 -2
- package/src/fs/json-size.js +2 -1
- package/src/fs/lines.js +49 -2
- package/src/fs/patch.js +26 -10
- package/src/fs/process-tree.js +19 -1
- package/src/fs/read-window.js +36 -20
- package/src/fs/source-window.js +5 -10
- package/src/fs/text-ops.js +38 -18
- package/src/fs/vfs.js +296 -41
- package/src/fs/workspace.js +24 -6
- package/src/output/bottleneck.js +50 -25
- package/src/output/final.js +20 -0
- package/src/output/format.js +97 -36
- package/src/output/outcome.js +16 -4
- package/src/runtime/batch-input.js +17 -3
- package/src/runtime/guest-api.js +32 -7
- package/src/runtime/guest-worker.js +22 -4
- package/src/runtime/parallel.js +9 -49
- package/src/runtime/program-batch.js +53 -28
- package/src/runtime/program-file.js +2 -1
- package/src/runtime/program.js +94 -41
- package/src/runtime/reference.js +10 -9
- package/src/runtime/runtime.js +69 -18
- package/src/runtime/worker-pool.js +10 -1
- package/src/shared/decode.js +9 -8
- package/src/shared/image-worker.js +7 -0
- package/src/shared/image.js +58 -15
- package/src/shared/png.js +18 -0
- package/src/shared/result.js +30 -9
- package/src/shared/syntax-context.js +7 -1
- package/src/shared/utf8.js +11 -13
- package/src/ui/host-render.js +4 -0
- package/src/ui/omp-frame.js +2 -0
- package/src/ui/progress.js +2 -1
- package/src/ui/render-measure.js +11 -0
- package/src/ui/render.js +92 -31
- package/src/ui/trace.js +8 -5
package/src/fs/check.js
CHANGED
|
@@ -6,24 +6,224 @@ const OPEN = { "{": "}", "[": "]", "(": ")" };
|
|
|
6
6
|
|
|
7
7
|
const CLOSE = new Set(["}", "]", ")"]);
|
|
8
8
|
|
|
9
|
-
const REGEX_PRECEDERS = new Set(["(", ",", "=", ":", "[", "!", "&", "|", "?", "{", "}", ";", "+", "-", "*", "%", "<", ">", "~", "^", "return", "typeof", "case", "do", "else", "in", "
|
|
9
|
+
const REGEX_PRECEDERS = new Set(["(", ",", "...", "=", "=>", ":", "[", "!", "&", "|", "?", "{", "}", ";", "+", "-", "*", "/", "%", "<", ">", "~", "^", "return", "throw", "typeof", "void", "delete", "await", "yield", "debugger", "new", "case", "do", "else", "in", "instanceof"]);
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
const CONTROL_PARENS = new Set(["if", "while", "for", "with"]);
|
|
12
|
+
|
|
13
|
+
const RESTRICTED_PRODUCTIONS = new Set(["return", "yield", "break", "continue"]);
|
|
14
|
+
|
|
15
|
+
const JS_EXT = new Set([".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx", ".mts", ".cts"]);
|
|
16
|
+
|
|
17
|
+
const OBJECT_PRECEDERS = new Set(["(", "[", "=", ",", "...", "?", "+", "-", "*", "/", "%", "<", ">", "&", "|", "^", "!", "~", "return", "throw", "typeof", "void", "delete", "yield", "await", "case", "in", "instanceof"]);
|
|
18
|
+
|
|
19
|
+
function skipString(text, i, quote, multiline = false, spliced = false) {
|
|
20
|
+
for (let j = i + quote.length; j < text.length; j++) {
|
|
13
21
|
if (text[j] === "\\") {
|
|
14
|
-
j
|
|
22
|
+
let escaped = j + 1;
|
|
23
|
+
|
|
24
|
+
// C/C++ removes line splices before interpreting escapes. An escape can
|
|
25
|
+
// therefore reach a quote across one or more physical backslash-newlines.
|
|
26
|
+
if (spliced) {
|
|
27
|
+
while (text.startsWith("\\\n", escaped) || text.startsWith("\\\r\n", escaped)) escaped += text[escaped + 1] === "\r" ? 3 : 2;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// CRLF is one escaped line terminator, not an escaped CR followed by bare LF.
|
|
31
|
+
j = escaped + (text[escaped] === "\r" && text[escaped + 1] === "\n" ? 1 : 0);
|
|
15
32
|
continue;
|
|
16
33
|
}
|
|
17
34
|
|
|
18
|
-
if (text
|
|
35
|
+
if (text.startsWith(quote, j)) return j + quote.length;
|
|
19
36
|
|
|
20
|
-
if (quote !== "`" && text[j] === "\n") return -1;
|
|
37
|
+
if (!multiline && quote !== "`" && text[j] === "\n") return -1;
|
|
21
38
|
}
|
|
22
39
|
|
|
23
40
|
return -1;
|
|
24
41
|
}
|
|
25
42
|
|
|
26
|
-
|
|
43
|
+
// C++ raw strings carry their own delimiter. Phase-two splices may split the
|
|
44
|
+
// encoding/R prefix, but the quoted raw payload keeps its original bytes.
|
|
45
|
+
function consumeCppRaw(text, i) {
|
|
46
|
+
const prefix = /(?:(?:u(?:\\\r?\n)*8|[uUL])(?:\\\r?\n)*)?R(?:\\\r?\n)*"/y;
|
|
47
|
+
prefix.lastIndex = i;
|
|
48
|
+
|
|
49
|
+
if (!prefix.test(text)) return null;
|
|
50
|
+
const start = prefix.lastIndex;
|
|
51
|
+
const opener = /^([^\s()\\]{0,16})\(/.exec(text.slice(start, start + 17));
|
|
52
|
+
|
|
53
|
+
if (!opener) return null;
|
|
54
|
+
const end = text.indexOf(")" + opener[1] + '"', start + opener[0].length);
|
|
55
|
+
|
|
56
|
+
return end < 0 ? { error: "unterminated raw string", at: i } : { end: end + opener[1].length + 2, prev: "value" };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Rust r#"..."#, br#"..."#, cr#"..."# (and r"...") treat quotes, braces, and
|
|
60
|
+
// backslashes as payload. r#ident is a raw identifier, not a string.
|
|
61
|
+
function consumeRustRaw(text, i) {
|
|
62
|
+
let j = i;
|
|
63
|
+
|
|
64
|
+
if (text[j] === "b" || text[j] === "c") j++;
|
|
65
|
+
|
|
66
|
+
if (text[j] !== "r") return null;
|
|
67
|
+
j++;
|
|
68
|
+
let hashes = 0;
|
|
69
|
+
|
|
70
|
+
while (text[j] === "#") {
|
|
71
|
+
hashes++;
|
|
72
|
+
j++;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (text[j] !== '"') return null;
|
|
76
|
+
const closer = '"' + "#".repeat(hashes);
|
|
77
|
+
const end = text.indexOf(closer, j + 1);
|
|
78
|
+
|
|
79
|
+
return end < 0 ? { error: "unterminated raw string", at: i } : { end: end + closer.length, prev: "value" };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Raw interpolation uses the dollar count as its brace delimiter. Nested C#
|
|
83
|
+
// expressions can contain quotes matching the enclosing raw-string delimiter.
|
|
84
|
+
function consumeCSharpRawInterpolation(text, i) {
|
|
85
|
+
const prefix = /(\$+)("{3,})/y;
|
|
86
|
+
prefix.lastIndex = i;
|
|
87
|
+
const opener = prefix.exec(text);
|
|
88
|
+
|
|
89
|
+
if (!opener) return null;
|
|
90
|
+
const width = opener[1].length;
|
|
91
|
+
const quote = opener[2];
|
|
92
|
+
|
|
93
|
+
for (let j = i + opener[0].length; j < text.length; j++) {
|
|
94
|
+
if (text.startsWith(quote, j)) return { end: j + quote.length, prev: "value" };
|
|
95
|
+
|
|
96
|
+
if (text[j] !== "{") continue;
|
|
97
|
+
let run = j + 1;
|
|
98
|
+
|
|
99
|
+
while (text[run] === "{") run++;
|
|
100
|
+
|
|
101
|
+
if (run - j < width) { j = run - 1; continue; }
|
|
102
|
+
|
|
103
|
+
const end = balancedEnd(text, run - 1, [], false, false, false, false, ".cs", true);
|
|
104
|
+
|
|
105
|
+
if (end < 0 || !text.startsWith("}".repeat(width - 1), end)) return { error: "unterminated string", at: i };
|
|
106
|
+
j = end + width - 2;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { error: "unterminated string", at: i };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// C# interpolation expressions are nested code, not enclosing-string payload.
|
|
113
|
+
// Verbatim payload escapes quotes by doubling them and leaves backslashes intact.
|
|
114
|
+
function consumeCSharpString(text, i) {
|
|
115
|
+
const raw = consumeCSharpRawInterpolation(text, i);
|
|
116
|
+
|
|
117
|
+
if (raw) return raw;
|
|
118
|
+
const opener = /^(?:\$@"|@\$"|@"|\$")/.exec(text.slice(i, i + 3));
|
|
119
|
+
|
|
120
|
+
if (!opener) return null;
|
|
121
|
+
const verbatim = opener[0].includes("@");
|
|
122
|
+
const interpolated = opener[0].includes("$");
|
|
123
|
+
|
|
124
|
+
for (let j = i + opener[0].length; j < text.length; j++) {
|
|
125
|
+
if (!verbatim && text[j] === "\\") { j++; continue; }
|
|
126
|
+
|
|
127
|
+
if (interpolated && text[j] === "{") {
|
|
128
|
+
if (text[j + 1] === "{") { j++; continue; }
|
|
129
|
+
|
|
130
|
+
const end = balancedEnd(text, j, [], false, false, false, false, ".cs", true);
|
|
131
|
+
|
|
132
|
+
if (end < 0) return { error: "unterminated string", at: i };
|
|
133
|
+
j = end - 1;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (text[j] === '"') {
|
|
138
|
+
if (verbatim && text[j + 1] === '"') { j++; continue; }
|
|
139
|
+
|
|
140
|
+
return { end: j + 1, prev: "value" };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!verbatim && text[j] === "\n") break;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { error: "unterminated string", at: i };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Kotlin templates contain nested Kotlin code. Raw payload has no backslash
|
|
150
|
+
// escapes, but both ordinary and raw strings interpolate ${...} expressions.
|
|
151
|
+
function consumeKotlinString(text, i) {
|
|
152
|
+
const quote = text.startsWith('"""', i) ? '"""' : '"';
|
|
153
|
+
const raw = quote.length === 3;
|
|
154
|
+
|
|
155
|
+
for (let j = i + quote.length; j < text.length; j++) {
|
|
156
|
+
if (!raw && text[j] === "\\") { j++; continue; }
|
|
157
|
+
|
|
158
|
+
if (text[j] === "$" && text[j + 1] === "{") {
|
|
159
|
+
const end = balancedEnd(text, j + 1, [], false, false, false, false, ".kt");
|
|
160
|
+
|
|
161
|
+
if (end < 0) return { error: "unterminated string", at: i };
|
|
162
|
+
j = end - 1;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (text.startsWith(quote, j)) return { end: j + quote.length, prev: "value" };
|
|
167
|
+
|
|
168
|
+
if (!raw && text[j] === "\n") break;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { error: "unterminated string", at: i };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Swift strings use matching hash counts on delimiters and escapes. Interpolation
|
|
175
|
+
// is nested Swift code; its quoted payload must not close the enclosing string.
|
|
176
|
+
function consumeSwiftString(text, i) {
|
|
177
|
+
let start = i;
|
|
178
|
+
|
|
179
|
+
while (text[start] === "#") start++;
|
|
180
|
+
|
|
181
|
+
if (text[start] !== '"') return null;
|
|
182
|
+
const hashes = text.slice(i, start);
|
|
183
|
+
const quote = text.startsWith('"""', start) ? '"""' : '"';
|
|
184
|
+
const closer = quote + hashes;
|
|
185
|
+
|
|
186
|
+
for (let j = start + quote.length; j < text.length; j++) {
|
|
187
|
+
if (text[j] === "\\" && text.startsWith(hashes, j + 1)) {
|
|
188
|
+
const escaped = j + hashes.length + 1;
|
|
189
|
+
|
|
190
|
+
if (text[escaped] === "(") {
|
|
191
|
+
const end = balancedEnd(text, escaped, [], false, false, false, false, ".swift");
|
|
192
|
+
|
|
193
|
+
if (end < 0) return { error: "unterminated string", at: i };
|
|
194
|
+
j = end - 1;
|
|
195
|
+
} else j = escaped;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (text.startsWith(closer, j)) return { end: j + closer.length, prev: "value" };
|
|
200
|
+
|
|
201
|
+
if (quote.length === 1 && text[j] === "\n") break;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return { error: "unterminated string", at: i };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Swift extended regexes close on / plus the opener's hash count. Bare slashes,
|
|
208
|
+
// quotes, brackets and multiline payload are pattern text, not source tokens.
|
|
209
|
+
function consumeSwiftRegex(text, i) {
|
|
210
|
+
let start = i;
|
|
211
|
+
|
|
212
|
+
while (text[start] === "#") start++;
|
|
213
|
+
|
|
214
|
+
if (start === i || text[start] !== "/") return null;
|
|
215
|
+
const closer = "/" + text.slice(i, start);
|
|
216
|
+
|
|
217
|
+
for (let j = start + 1; j < text.length; j++) {
|
|
218
|
+
if (text[j] === "\\") { j++; continue; }
|
|
219
|
+
|
|
220
|
+
if (text.startsWith(closer, j)) return { end: j + closer.length, prev: "value" };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return { error: "unterminated regex", at: i };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function skipTemplate(text, i, stack, stringExt) {
|
|
27
227
|
for (let j = i + 1; j < text.length; j++) {
|
|
28
228
|
if (text[j] === "\\") {
|
|
29
229
|
j++;
|
|
@@ -33,7 +233,7 @@ function skipTemplate(text, i, stack) {
|
|
|
33
233
|
if (text[j] === "`") return j + 1;
|
|
34
234
|
|
|
35
235
|
if (text[j] === "$" && text[j + 1] === "{") {
|
|
36
|
-
const end = balancedEnd(text, j + 1, stack);
|
|
236
|
+
const end = balancedEnd(text, j + 1, stack, false, false, false, true, stringExt);
|
|
37
237
|
|
|
38
238
|
if (end < 0) return -1;
|
|
39
239
|
j = end - 1; // loop increment lands on the char after "}"
|
|
@@ -43,24 +243,63 @@ function skipTemplate(text, i, stack) {
|
|
|
43
243
|
return -1;
|
|
44
244
|
}
|
|
45
245
|
|
|
46
|
-
/** Index just past the
|
|
47
|
-
function balancedEnd(text, i, stack) {
|
|
246
|
+
/** Index just past the matching bracket at i, scanning nested code; −1 when unbalanced. */
|
|
247
|
+
function balancedEnd(text, i, stack, rust = false, cpp = false, go = false, js = true, stringExt = "", csharpInterpolation = false) {
|
|
48
248
|
const depth = stack.length;
|
|
49
|
-
const r = scan(text, i, stack, depth);
|
|
249
|
+
const r = scan(text, i, stack, depth, rust, cpp, go, js, stringExt, csharpInterpolation);
|
|
50
250
|
|
|
51
251
|
return r.error ? -1 : r.end;
|
|
52
252
|
}
|
|
53
253
|
|
|
54
|
-
function skipComment(text, i) {
|
|
55
|
-
|
|
56
|
-
|
|
254
|
+
function skipComment(text, i, nested = false, js = false, stringExt = "") {
|
|
255
|
+
const spliced = [".c", ".cc", ".cpp", ".h", ".hpp"].includes(stringExt);
|
|
256
|
+
// Translation-phase line splices can split either comment opener. Keep the
|
|
257
|
+
// physical offsets so editable spans still include every original source line.
|
|
258
|
+
const opener = spliced ? /\/(?:\\\r?\n)*([/*])/y : /\/([/*])/y;
|
|
259
|
+
opener.lastIndex = i;
|
|
260
|
+
const matchOpener = opener.exec(text);
|
|
261
|
+
|
|
262
|
+
if (!matchOpener) return null;
|
|
263
|
+
const start = opener.lastIndex;
|
|
264
|
+
|
|
265
|
+
if (matchOpener[1] === "/") {
|
|
266
|
+
// Java admits CR/LF; C# additionally admits NEXT_LINE and Unicode separators.
|
|
267
|
+
const newline = js ? /[\r\n\u2028\u2029]/g
|
|
268
|
+
: stringExt === ".cs" ? /[\r\n\u0085\u2028\u2029]/g
|
|
269
|
+
: stringExt === ".java" ? /[\r\n]/g : /\n/g;
|
|
270
|
+
|
|
271
|
+
newline.lastIndex = start;
|
|
272
|
+
let match;
|
|
57
273
|
|
|
58
|
-
|
|
274
|
+
while ((match = newline.exec(text))) {
|
|
275
|
+
// C/C++ removes backslash-newline before comments are recognized. Even
|
|
276
|
+
// a pair of backslashes continues the comment; these are not string escapes.
|
|
277
|
+
const before = match.index - (text[match.index - 1] === "\r" ? 2 : 1);
|
|
278
|
+
|
|
279
|
+
if (!spliced || text[before] !== "\\") return match.index;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return text.length;
|
|
59
283
|
}
|
|
60
284
|
|
|
61
|
-
|
|
285
|
+
// Rust, Kotlin and Swift block comments nest; other dialects close at the first */.
|
|
286
|
+
// C/C++ line splices can split that closer without changing source coordinates.
|
|
287
|
+
const closer = spliced ? /\*(?:\\\r?\n)*\//y : /\*\//y;
|
|
288
|
+
let depth = 1;
|
|
289
|
+
|
|
290
|
+
for (let j = start; j < text.length - 1; j++) {
|
|
291
|
+
if (nested && text[j] === "/" && text[j + 1] === "*") { depth++; j++; }
|
|
292
|
+
else if (text[j] === "*") {
|
|
293
|
+
closer.lastIndex = j;
|
|
62
294
|
|
|
63
|
-
|
|
295
|
+
if (!closer.test(text)) continue;
|
|
296
|
+
|
|
297
|
+
if (--depth === 0) return closer.lastIndex;
|
|
298
|
+
j = closer.lastIndex - 1;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return -1;
|
|
64
303
|
}
|
|
65
304
|
|
|
66
305
|
function skipRegex(text, i) {
|
|
@@ -92,51 +331,198 @@ function lineOf(text, i) {
|
|
|
92
331
|
return n;
|
|
93
332
|
}
|
|
94
333
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
334
|
+
// Consume full identifiers, including Unicode escapes, so payload braces and
|
|
335
|
+
// keyword suffixes never become code tokens. Preserve the spelling for context.
|
|
336
|
+
const IDENTIFIER = /(?:[$_\p{ID_Start}]|\\u(?:[\da-fA-F]{4}|\{[\da-fA-F]+\}))(?:[$\u200C\u200D\p{ID_Continue}]|\\u(?:[\da-fA-F]{4}|\{[\da-fA-F]+\}))*/uy;
|
|
98
337
|
|
|
99
338
|
function readIdentifier(text, i) {
|
|
100
|
-
|
|
339
|
+
IDENTIFIER.lastIndex = i;
|
|
340
|
+
|
|
341
|
+
return IDENTIFIER.test(text) ? IDENTIFIER.lastIndex : i;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function identifierContext(word, prev, stack, stringExt) {
|
|
345
|
+
// TypeScript assertion operators expect a type; a following object type closes
|
|
346
|
+
// the asserted operand, not a statement block. Bare identifiers stay operands.
|
|
347
|
+
if ([".ts", ".tsx", ".mts", ".cts"].includes(stringExt) && ["as", "satisfies"].includes(word)
|
|
348
|
+
&& !REGEX_PRECEDERS.has(prev)
|
|
349
|
+
&& !["", ".", "const", "let", "var", "function", "class", "interface", "enum", "namespace", "module", "type"].includes(prev)) return "=";
|
|
101
350
|
|
|
102
|
-
|
|
351
|
+
// `of` is an identifier except after the binding in a for-of header.
|
|
352
|
+
const forOf = word === "of" && stack.at(-1)?.forOf === true && prev !== "."
|
|
353
|
+
&& !["", "const", "let", "var"].includes(prev) && (prev === "}" || !REGEX_PRECEDERS.has(prev));
|
|
103
354
|
|
|
104
|
-
return
|
|
355
|
+
return prev === "." ? "value" : forOf ? "=" : word;
|
|
105
356
|
}
|
|
106
357
|
|
|
107
|
-
function
|
|
358
|
+
function punctuationContext(text, i, stack, conditionals, js) {
|
|
108
359
|
const c = text[i];
|
|
109
360
|
|
|
361
|
+
// Property values and conditional alternatives begin expressions; labels and
|
|
362
|
+
// switch cases begin statements. Match a conditional colon at its own depth.
|
|
363
|
+
if (js && c === "?" && !["?", "."].includes(text[i + 1]) && text[i - 1] !== "?") conditionals.push(stack.length);
|
|
364
|
+
|
|
365
|
+
if (js && c === ":") {
|
|
366
|
+
const conditional = conditionals.at(-1) === stack.length;
|
|
367
|
+
|
|
368
|
+
if (conditional) conditionals.pop();
|
|
369
|
+
|
|
370
|
+
if (conditional || stack.at(-1)?.object) return "=";
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return c;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function consumeQuoted(text, i, stack, go, rust, stringExt) {
|
|
377
|
+
const c = text[i];
|
|
378
|
+
|
|
379
|
+
// Go raw strings and Kotlin escaped identifiers have neither escapes nor
|
|
380
|
+
// JavaScript template interpolation.
|
|
381
|
+
if ((go || stringExt === ".kt") && c === "`") {
|
|
382
|
+
const end = text.indexOf("`", i + 1);
|
|
383
|
+
|
|
384
|
+
return end < 0 ? { error: go ? "unterminated raw string" : "unterminated escaped identifier", at: i } : { end: end + 1, prev: "value" };
|
|
385
|
+
}
|
|
386
|
+
|
|
110
387
|
if (c === "`") {
|
|
111
|
-
const end = skipTemplate(text, i, stack);
|
|
388
|
+
const end = skipTemplate(text, i, stack, stringExt);
|
|
112
389
|
|
|
113
390
|
return end < 0 ? { error: "unterminated template literal", at: i } : { end, prev: "value" };
|
|
114
391
|
}
|
|
115
392
|
|
|
116
|
-
|
|
393
|
+
// C# raw strings have no backslash escapes; their closers match the opener's
|
|
394
|
+
// whole quote run, not merely three quotes.
|
|
395
|
+
if (stringExt === ".cs" && text.startsWith('"""', i)) {
|
|
396
|
+
const quote = /^"{3,}/.exec(text.slice(i))[0];
|
|
397
|
+
const end = text.indexOf(quote, i + quote.length);
|
|
398
|
+
|
|
399
|
+
return end < 0 ? { error: "unterminated string", at: i } : { end: end + quote.length, prev: "value" };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Java text blocks close on an unescaped triple
|
|
403
|
+
// delimiter; individual quotes and physical newlines are payload.
|
|
404
|
+
const quote = stringExt === ".java" && text.startsWith('"""', i) ? '"""' : c;
|
|
405
|
+
const end = skipString(text, i, quote, quote.length === 3 || rust && c === '"', [".c", ".cc", ".cpp", ".h", ".hpp"].includes(stringExt));
|
|
117
406
|
|
|
118
407
|
return end < 0 ? { error: "unterminated string", at: i } : { end, prev: "value" };
|
|
119
408
|
}
|
|
120
409
|
|
|
121
|
-
function consumeSlash(text, i, prev) {
|
|
122
|
-
|
|
123
|
-
const end = skipComment(text, i);
|
|
410
|
+
function consumeSlash(text, i, prev, rust, js, stringExt) {
|
|
411
|
+
const commentEnd = skipComment(text, i, rust || stringExt === ".kt" || stringExt === ".swift", js, stringExt);
|
|
124
412
|
|
|
125
|
-
|
|
413
|
+
if (commentEnd !== null) {
|
|
414
|
+
if (commentEnd < 0) return { error: "unterminated comment", at: i };
|
|
126
415
|
|
|
127
|
-
|
|
416
|
+
// Restricted productions end at a line break, including one in a comment.
|
|
417
|
+
const restricted = RESTRICTED_PRODUCTIONS.has(prev);
|
|
418
|
+
|
|
419
|
+
return { end: commentEnd, prev: restricted && /[\r\n\u2028\u2029]/.test(text.slice(i, commentEnd)) ? ";" : prev, trivia: true };
|
|
128
420
|
}
|
|
129
421
|
|
|
130
|
-
|
|
422
|
+
// Slash-delimited regex literals belong to JS/TS and Swift, not the other
|
|
423
|
+
// scanned dialects where JavaScript keywords can be ordinary identifiers.
|
|
424
|
+
if (!js && stringExt !== ".swift") return null;
|
|
425
|
+
|
|
426
|
+
// JS/TS class heritage and Swift try begin operands, not completed identifiers.
|
|
427
|
+
if (prev !== "" && !REGEX_PRECEDERS.has(prev) && !(js && prev === "extends") && !(stringExt === ".swift" && prev === "try")) return null;
|
|
131
428
|
const end = skipRegex(text, i);
|
|
132
429
|
|
|
133
430
|
return end > 0 ? { end, prev: "value" } : null;
|
|
134
431
|
}
|
|
135
432
|
|
|
136
|
-
|
|
137
|
-
|
|
433
|
+
function nextExpressionToken(text, i, allowNewline = true) {
|
|
434
|
+
while (i < text.length) {
|
|
435
|
+
if (/\s/.test(text[i])) {
|
|
436
|
+
if (!allowNewline && /[\r\n\u2028\u2029]/.test(text[i])) return -1;
|
|
437
|
+
i++;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (!text.startsWith("/*", i) && !text.startsWith("//", i)) break;
|
|
442
|
+
const end = skipComment(text, i, false, true);
|
|
443
|
+
|
|
444
|
+
if (end < 0 || !allowNewline && /[\r\n\u2028\u2029]/.test(text.slice(i, end))) return -1;
|
|
445
|
+
i = end;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return i;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function expressionBlockKeyword(text, i) {
|
|
452
|
+
let end = readIdentifier(text, i);
|
|
453
|
+
let word = text.slice(i, end);
|
|
454
|
+
|
|
455
|
+
if (word === "async") {
|
|
456
|
+
const next = nextExpressionToken(text, end, false);
|
|
457
|
+
|
|
458
|
+
if (next < 0) return "";
|
|
459
|
+
end = readIdentifier(text, next);
|
|
460
|
+
word = text.slice(next, end);
|
|
461
|
+
|
|
462
|
+
if (word !== "function") return "";
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (word !== "function" && word !== "class") return "";
|
|
466
|
+
const next = nextExpressionToken(text, end);
|
|
467
|
+
|
|
468
|
+
// Keyword-named properties are not block expressions.
|
|
469
|
+
if (next < 0 || readIdentifier(text, next) === next && !(word === "function" ? ["(", "*"].includes(text[next]) : text[next] === "{")) return "";
|
|
470
|
+
|
|
471
|
+
return word;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function consumeExpressionBlock(text, i, stack, stringExt) {
|
|
475
|
+
const keyword = expressionBlockKeyword(text, i);
|
|
476
|
+
|
|
477
|
+
if (!keyword) return null;
|
|
478
|
+
const { open } = firstDeclarationOpen(text, i, text.length, false, false, false, true, keyword === "function", stringExt);
|
|
479
|
+
|
|
480
|
+
if (open < 0) return null;
|
|
481
|
+
const result = scan(text, open, stack, stack.length, false, false, false, true, stringExt);
|
|
482
|
+
|
|
483
|
+
return result.error ? result : { end: result.end, prev: "value" };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** Consume a literal, expression object, comment, or update operator at i. Returns { end, prev } | { error, at } | null. */
|
|
487
|
+
function consumeLiteral(text, i, stack, prev, rust, cpp, go, js, prevEnd, stringExt) {
|
|
138
488
|
const c = text[i];
|
|
139
489
|
|
|
490
|
+
if (cpp && ["R", "u", "U", "L"].includes(c)) {
|
|
491
|
+
const raw = consumeCppRaw(text, i);
|
|
492
|
+
|
|
493
|
+
if (raw) return raw;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// C++ numeric tokens can contain apostrophe digit separators, exponent signs
|
|
497
|
+
// and suffixes. Phase-two splices can split those tokens; retain their physical
|
|
498
|
+
// extent without treating a continued separator as a character-literal opener.
|
|
499
|
+
if (cpp && (/[0-9]/.test(c) || c === "." && /[0-9]/.test(text[i + 1]))) {
|
|
500
|
+
const number = /^(?:[0-9]|\.[0-9])(?:[eEpP](?:\\\r?\n)*[+-]|[\w.]|'(?:\\\r?\n)*[\w]|\\\r?\n)*/.exec(text.slice(i));
|
|
501
|
+
|
|
502
|
+
return { end: i + number[0].length, prev: "value" };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Consume JS numeric operands atomically: a decimal's trailing dot is not a
|
|
506
|
+
// member operator, whereas the second dot in 42..valueOf() is.
|
|
507
|
+
if (js && (/[0-9]/.test(c) || c === "." && /[0-9]/.test(text[i + 1]))) {
|
|
508
|
+
const number = /^(?:0[xX][\da-fA-F_]+|0[bB][01_]+|0[oO][0-7_]+|(?:\d[\d_]*\.?[\d_]*|\.\d[\d_]*)(?:[eE][+-]?[\d_]+)?)n?/.exec(text.slice(i));
|
|
509
|
+
|
|
510
|
+
return { end: i + number[0].length, prev: "value" };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (rust && ["r", "b", "c"].includes(c)) {
|
|
514
|
+
const raw = consumeRustRaw(text, i);
|
|
515
|
+
|
|
516
|
+
if (raw) return raw;
|
|
517
|
+
|
|
518
|
+
// A raw identifier is an operand, not the keyword after its r# prefix.
|
|
519
|
+
if (c === "r" && text[i + 1] === "#") {
|
|
520
|
+
const end = readIdentifier(text, i + 2);
|
|
521
|
+
|
|
522
|
+
if (end > i + 2) return { end, prev: "value" };
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
140
526
|
if (rust && c === "'") {
|
|
141
527
|
const lifetime = /^'[\p{ID_Start}_][\p{ID_Continue}]*/u.exec(text.slice(i));
|
|
142
528
|
const end = i + (lifetime?.[0].length ?? 0);
|
|
@@ -145,17 +531,84 @@ function consumeLiteral(text, i, stack, prev, rust) {
|
|
|
145
531
|
if (lifetime && text[end] !== "'") return { end, prev: "value" };
|
|
146
532
|
}
|
|
147
533
|
|
|
148
|
-
if (
|
|
534
|
+
if (stringExt === ".cs" && (c === "@" || c === "$")) {
|
|
535
|
+
const literal = consumeCSharpString(text, i);
|
|
536
|
+
|
|
537
|
+
if (literal) return literal;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// C# verbatim identifiers keep escaped keywords in operand context.
|
|
541
|
+
if (stringExt === ".cs" && c === "@") {
|
|
542
|
+
const end = readIdentifier(text, i + 1);
|
|
543
|
+
|
|
544
|
+
if (end > i + 1) return { end, prev: "value" };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (stringExt === ".swift" && (c === "#" || c === '"')) {
|
|
548
|
+
const literal = consumeSwiftString(text, i) ?? consumeSwiftRegex(text, i);
|
|
549
|
+
|
|
550
|
+
if (literal) return literal;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (stringExt === ".kt" && c === '"') return consumeKotlinString(text, i);
|
|
554
|
+
|
|
555
|
+
if (['"', "'", "`"].includes(c)) return consumeQuoted(text, i, stack, go, rust, stringExt);
|
|
556
|
+
|
|
557
|
+
// An arrow is not a comparison operator; its brace opens a body, not an object.
|
|
558
|
+
if (js && text.startsWith("=>", i)) return { end: i + 2, prev: "=>" };
|
|
559
|
+
|
|
560
|
+
// A private name is one operand token, even when its name is a keyword.
|
|
561
|
+
if (js && c === "#") {
|
|
562
|
+
const end = readIdentifier(text, i + 1);
|
|
563
|
+
|
|
564
|
+
if (end > i + 1) return { end, prev: "value" };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Spread introduces an operand; its final dot is not member access.
|
|
568
|
+
if (js && c === "." && text.startsWith("...", i)) return { end: i + 3, prev: "..." };
|
|
569
|
+
|
|
570
|
+
// Postfix ! closes an operand. C# permits trivia; TypeScript excludes line
|
|
571
|
+
// breaks; Swift requires left adjacency and keeps try! in prefix context.
|
|
572
|
+
if (c === "!" && text[i + 1] !== "=" && prev !== "" && !REGEX_PRECEDERS.has(prev)
|
|
573
|
+
&& (stringExt === ".cs" || js && !/[\r\n\u2028\u2029]/.test(text.slice(prevEnd, i))
|
|
574
|
+
|| stringExt === ".swift" && prevEnd === i && prev !== "try")) return { end: i + 1, prev: "value" };
|
|
575
|
+
|
|
576
|
+
// Function/class expression bodies close a value; declarations still close a
|
|
577
|
+
// statement. Heritage constructors can themselves be function/class expressions.
|
|
578
|
+
if (js && (OBJECT_PRECEDERS.has(prev) || prev === "new" || prev === "extends")) {
|
|
579
|
+
const expression = consumeExpressionBlock(text, i, stack, stringExt);
|
|
580
|
+
|
|
581
|
+
if (expression) return expression;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Expression aggregates close a value, not a statement block. Consume array
|
|
585
|
+
// bindings too so the declaration scanner can retain trailing continuations.
|
|
586
|
+
if (c === "{" && OBJECT_PRECEDERS.has(prev) && (js || prev !== "<" && prev !== ">")
|
|
587
|
+
|| js && c === "[" && prev === "=") {
|
|
588
|
+
const depth = stack.length;
|
|
589
|
+
stack.push({ c, at: i, object: c === "{" });
|
|
590
|
+
const result = scan(text, i + 1, stack, depth, rust, cpp, go, js, stringExt);
|
|
591
|
+
|
|
592
|
+
return result.error ? result : { end: result.end, prev: "value" };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Only postfix updates close a value. Prefix updates still expect an operand,
|
|
596
|
+
// and JavaScript postfix updates cannot cross a line terminator, even in a comment.
|
|
597
|
+
if ((c === "+" || c === "-") && text[i + 1] === c) {
|
|
598
|
+
const prefix = prev === "" || REGEX_PRECEDERS.has(prev) || js && /[\r\n\u2028\u2029]/.test(text.slice(prevEnd, i));
|
|
599
|
+
|
|
600
|
+
return { end: i + 2, prev: prefix ? c : "value" };
|
|
601
|
+
}
|
|
149
602
|
|
|
150
603
|
if (c !== "/") return null;
|
|
151
604
|
|
|
152
|
-
return consumeSlash(text, i, prev);
|
|
605
|
+
return consumeSlash(text, i, prev, rust, js, stringExt);
|
|
153
606
|
}
|
|
154
607
|
|
|
155
608
|
/** Push/pop a bracket; returns an error, a stop, or null to continue. */
|
|
156
|
-
function bracket(c, i, stack, stopDepth) {
|
|
609
|
+
function bracket(c, i, stack, stopDepth, prev, js) {
|
|
157
610
|
if (OPEN[c]) {
|
|
158
|
-
stack.push({ c, at: i });
|
|
611
|
+
stack.push({ c, at: i, control: c === "(" && CONTROL_PARENS.has(prev), forOf: js && c === "(" && prev === "for" });
|
|
159
612
|
|
|
160
613
|
return null;
|
|
161
614
|
}
|
|
@@ -171,39 +624,384 @@ function bracket(c, i, stack, stopDepth) {
|
|
|
171
624
|
}
|
|
172
625
|
|
|
173
626
|
/** Skips comments, strings, templates and regex literals; `prev` is the last code token, which decides regex-vs-division. */
|
|
174
|
-
function scan(text, start, stack, stopDepth, rust = false) {
|
|
627
|
+
function scan(text, start, stack, stopDepth, rust = false, cpp = false, go = false, js = true, stringExt = "", csharpInterpolation = false) {
|
|
628
|
+
const conditionals = [];
|
|
175
629
|
let i = start;
|
|
176
630
|
let prev = "";
|
|
631
|
+
let prevEnd = start;
|
|
177
632
|
|
|
178
633
|
while (i < text.length) {
|
|
179
634
|
const c = text[i];
|
|
180
|
-
|
|
635
|
+
|
|
636
|
+
if (RESTRICTED_PRODUCTIONS.has(prev) && /[\r\n\u2028\u2029]/.test(c)) prev = ";";
|
|
637
|
+
const literal = consumeLiteral(text, i, stack, prev, rust, cpp, go, js, prevEnd, stringExt);
|
|
181
638
|
|
|
182
639
|
if (literal) {
|
|
183
640
|
if (literal.error) return literal;
|
|
184
641
|
i = literal.end;
|
|
185
642
|
prev = literal.prev;
|
|
643
|
+
prevEnd = literal.trivia ? prevEnd : i;
|
|
186
644
|
continue;
|
|
187
645
|
}
|
|
188
646
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
647
|
+
// A top-level interpolation colon starts a format string, not C# code.
|
|
648
|
+
if (csharpInterpolation && c === ":" && stack.length === stopDepth + 1) {
|
|
649
|
+
const end = text.indexOf("}", i + 1);
|
|
650
|
+
|
|
651
|
+
if (end < 0) return { error: "unterminated string", at: i };
|
|
652
|
+
i = end;
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const j = readIdentifier(text, i);
|
|
657
|
+
|
|
658
|
+
if (j > i) {
|
|
659
|
+
const word = text.slice(i, j);
|
|
660
|
+
|
|
661
|
+
// Preserve jump labels until their line break, and `for await` control headers.
|
|
662
|
+
if (prev !== "break" && prev !== "continue" && (prev !== "for" || word !== "await")) prev = identifierContext(word, prev, stack, stringExt);
|
|
663
|
+
|
|
192
664
|
i = j;
|
|
665
|
+
prevEnd = i;
|
|
193
666
|
continue;
|
|
194
667
|
}
|
|
195
668
|
|
|
196
|
-
|
|
669
|
+
// Closing a control condition starts a statement, unlike closing a call or
|
|
670
|
+
// grouped expression. Nested parens retain their own division context.
|
|
671
|
+
const closesControl = c === ")" && stack.at(-1)?.control === true;
|
|
672
|
+
const outcome = bracket(c, i, stack, stopDepth, prev, js);
|
|
197
673
|
|
|
198
674
|
if (outcome) return outcome;
|
|
199
675
|
|
|
200
|
-
if (!/\s/.test(c))
|
|
676
|
+
if (!/\s/.test(c)) {
|
|
677
|
+
prev = closesControl ? ";" : punctuationContext(text, i, stack, conditionals, js);
|
|
678
|
+
prevEnd = i + 1;
|
|
679
|
+
}
|
|
680
|
+
|
|
201
681
|
i++;
|
|
202
682
|
}
|
|
203
683
|
|
|
204
684
|
return { end: i };
|
|
205
685
|
}
|
|
206
686
|
|
|
687
|
+
function offsetOfLine(text, line) {
|
|
688
|
+
if (line <= 1) return 0;
|
|
689
|
+
let n = 1;
|
|
690
|
+
|
|
691
|
+
for (let i = 0; i < text.length; i++) {
|
|
692
|
+
if (text[i] === "\n" && ++n === line) return i + 1;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return -1;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function firstDeclarationOpen(text, start, limit, rust, cpp, go, js, typedSignature = false, stringExt = "", bodyExpected = false) {
|
|
699
|
+
const stack = [];
|
|
700
|
+
const conditionals = [];
|
|
701
|
+
let i = start;
|
|
702
|
+
let prev = "";
|
|
703
|
+
let prevEnd = start;
|
|
704
|
+
let depth = 0;
|
|
705
|
+
let returnType = false;
|
|
706
|
+
let predicateTarget = false;
|
|
707
|
+
let genericDepth = 0;
|
|
708
|
+
let parametersStarted = false;
|
|
709
|
+
let heritageHeader = false;
|
|
710
|
+
let bindingType = false;
|
|
711
|
+
let typeAlias = false;
|
|
712
|
+
|
|
713
|
+
while (i < text.length) {
|
|
714
|
+
if (i >= limit) {
|
|
715
|
+
// Declaration names can put parameters or TypeScript generics after trivia.
|
|
716
|
+
// Completed overloads must not resume at a later expression opener.
|
|
717
|
+
const nextParameters = js && bodyExpected && !parametersStarted
|
|
718
|
+
&& text[nextExpressionToken(text, i)] === "(";
|
|
719
|
+
|
|
720
|
+
const nextGenerics = typedSignature && !parametersStarted
|
|
721
|
+
&& text[nextExpressionToken(text, i)] === "<";
|
|
722
|
+
|
|
723
|
+
// Wrapped parameters, generic headers and return types remain unfinished
|
|
724
|
+
// inside delimiters. A next-line union/intersection also continues a return
|
|
725
|
+
// type; completed overloads still stop here.
|
|
726
|
+
const nextTypeOperator = typedSignature && returnType && prev !== ";"
|
|
727
|
+
&& ["|", "&"].includes(text[nextExpressionToken(text, i)]);
|
|
728
|
+
|
|
729
|
+
// `asserts` is a prefix only before an assertion target, not when an
|
|
730
|
+
// overload returns a type alias named asserts.
|
|
731
|
+
const target = prev === "asserts" ? nextExpressionToken(text, i) : -1;
|
|
732
|
+
const targetEnd = target < 0 ? -1 : readIdentifier(text, target);
|
|
733
|
+
const afterTarget = targetEnd > target ? nextExpressionToken(text, targetEnd, false) : -1;
|
|
734
|
+
|
|
735
|
+
const nextAssertionTarget = targetEnd > target
|
|
736
|
+
&& !/^(?:export|function|class|const|var|let|import|enum|default)$/.test(text.slice(target, targetEnd))
|
|
737
|
+
&& (afterTarget < 0 || afterTarget === text.length || ["{", ";"].includes(text[afterTarget])
|
|
738
|
+
|| text.slice(afterTarget, readIdentifier(text, afterTarget)) === "is");
|
|
739
|
+
|
|
740
|
+
// Bindings remain unfinished after assignment, an arrow, a constructor
|
|
741
|
+
// prefix or an operand-introducing operator, including intervening trivia.
|
|
742
|
+
const nextInitializer = js && (prev === "=" || !bodyExpected && (prev === "=>" || prev === "new" || OBJECT_PRECEDERS.has(prev)));
|
|
743
|
+
|
|
744
|
+
// A leading member operator continues the previous initializer, unlike a
|
|
745
|
+
// leading decimal literal, which can start a separate statement after ASI.
|
|
746
|
+
const nextMember = js && !bodyExpected && prev !== ";"
|
|
747
|
+
&& (prev === "." || /^(?:\.(?![\d.])|\?\.(?!\d))/.test(text.slice(nextExpressionToken(text, i))));
|
|
748
|
+
|
|
749
|
+
// Binary operators and declarator commas can lead continuation lines, but
|
|
750
|
+
// a next-line ++/-- starts a separate prefix-update statement under ASI.
|
|
751
|
+
const next = nextExpressionToken(text, i);
|
|
752
|
+
|
|
753
|
+
const nextBinary = js && !bodyExpected && prev !== ";"
|
|
754
|
+
&& (/^(?:\+(?!\+)|-(?!-)|[*/%<>&|^,]|[=!]=|=(?!>)|\?\?)/.test(text.slice(next))
|
|
755
|
+
|| ["in", "instanceof"].includes(text.slice(next, readIdentifier(text, next))));
|
|
756
|
+
|
|
757
|
+
// Conditional branches continue at their matching colon, not a label.
|
|
758
|
+
const nextConditional = js && !bodyExpected && prev !== ";"
|
|
759
|
+
&& (text[next] === "?" || text[next] === ":" && conditionals.at(-1) === stack.length);
|
|
760
|
+
|
|
761
|
+
// A tagged template can follow its tag across trivia without ending the
|
|
762
|
+
// binding. A semicolon instead starts a separate untagged expression.
|
|
763
|
+
const nextTaggedTemplate = js && !bodyExpected && prev !== ";" && text[next] === "`";
|
|
764
|
+
|
|
765
|
+
// Calls may put arguments on later lines and continue after their closer.
|
|
766
|
+
// A semicolon instead starts a separate parenthesized statement.
|
|
767
|
+
const nextCall = js && !bodyExpected && prev !== ";" && (text[next] === "(" || depth > 0);
|
|
768
|
+
|
|
769
|
+
// Computed members continue across trivia, including wrapped keys. An
|
|
770
|
+
// explicit semicolon instead begins a standalone array expression.
|
|
771
|
+
const nextComputedMember = js && !bodyExpected && prev !== ";"
|
|
772
|
+
&& (text[next] === "[" || stack.some(frame => frame.c === "["));
|
|
773
|
+
|
|
774
|
+
// Class/interface declarations cannot finish before their body, including
|
|
775
|
+
// wrapped heritage clauses. Keyword-named methods remain ordinary headers.
|
|
776
|
+
if (!heritageHeader && !nextInitializer && !nextMember && !nextBinary && !nextConditional && !nextTaggedTemplate && !nextCall && !nextComputedMember && !nextParameters && !nextGenerics && (!typedSignature || !(depth || genericDepth || returnType && (stack.length || nextTypeOperator || nextAssertionTarget
|
|
777
|
+
|| [":", "|", "&", "<", ",", "?", "[", "=>", "keyof", "readonly", "typeof", "extends", "type-predicate"].includes(prev))))) break;
|
|
778
|
+
const lineEnd = text.indexOf("\n", i + 1);
|
|
779
|
+
limit = lineEnd < 0 ? text.length : lineEnd;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// A binding's block arrow is an initializer value. Keep scanning for later
|
|
783
|
+
// declarators instead of ending the binding at this function body.
|
|
784
|
+
if (js && !bodyExpected && text[i] === "{" && prev === "=>") {
|
|
785
|
+
const end = balancedEnd(text, i, [], rust, cpp, go, js, stringExt);
|
|
786
|
+
|
|
787
|
+
if (end < 0) return { open: -1, end: text.length };
|
|
788
|
+
i = end;
|
|
789
|
+
prev = "value";
|
|
790
|
+
prevEnd = i;
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// A declaration signature ending in a generic > still opens its real body.
|
|
795
|
+
if (typedSignature && depth === 0 && text[i] === "{" && prev === ">") return { open: i, end: i };
|
|
796
|
+
const literal = consumeLiteral(text, i, stack, prev, rust, cpp, go, js, prevEnd, stringExt);
|
|
797
|
+
|
|
798
|
+
if (literal) {
|
|
799
|
+
if (literal.error) return { open: -1, end: limit };
|
|
800
|
+
i = literal.end;
|
|
801
|
+
|
|
802
|
+
// A multiline generic default is consumed as an expression object. Its
|
|
803
|
+
// closing line still contains the signature and possibly the body opener.
|
|
804
|
+
if (typedSignature && depth === 0 && i > limit) {
|
|
805
|
+
const lineEnd = text.indexOf("\n", i);
|
|
806
|
+
limit = lineEnd < 0 ? text.length : lineEnd;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
prev = literal.prev;
|
|
810
|
+
prevEnd = literal.trivia ? prevEnd : i;
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const j = readIdentifier(text, i);
|
|
815
|
+
|
|
816
|
+
if (j > i) {
|
|
817
|
+
const word = text.slice(i, j);
|
|
818
|
+
|
|
819
|
+
if (js && bodyExpected && depth === 0 && genericDepth === 0
|
|
820
|
+
&& (word === "class" || typedSignature && word === "interface")) {
|
|
821
|
+
const next = nextExpressionToken(text, j);
|
|
822
|
+
|
|
823
|
+
if (word === "class" ? expressionBlockKeyword(text, i) === "class" : next >= 0 && readIdentifier(text, next) > next) heritageHeader = true;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (js && depth === 0 && word === "type" && ["", "export", "declare"].includes(prev)
|
|
827
|
+
&& [".ts", ".tsx", ".mts", ".cts"].includes(stringExt)) typeAlias = true;
|
|
828
|
+
|
|
829
|
+
const typePredicate = predicateTarget && word === "is";
|
|
830
|
+
// `is` is contextual: a return alias named is (including typeof is) is
|
|
831
|
+
// not a predicate. Only a leading target, optionally after asserts, is.
|
|
832
|
+
predicateTarget = returnType && [":", "asserts"].includes(prev)
|
|
833
|
+
&& !["asserts", "typeof"].includes(word);
|
|
834
|
+
// Binding annotations and type aliases introduce a type operand; in
|
|
835
|
+
// value initializers these words stay identifiers.
|
|
836
|
+
prev = (bindingType || typeAlias) && ["keyof", "readonly", "unique", "infer", "abstract", "extends"].includes(word) ? "="
|
|
837
|
+
: typePredicate ? "type-predicate" : prev === "for" && word === "await" ? prev : identifierContext(word, prev, stack, stringExt);
|
|
838
|
+
i = j;
|
|
839
|
+
prevEnd = i;
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
if (typedSignature && depth === 0) {
|
|
844
|
+
if (text[i] === "<") genericDepth++;
|
|
845
|
+
else if (text[i] === ">") genericDepth = Math.max(0, genericDepth - 1);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if (text[i] === "(") {
|
|
849
|
+
if (depth === 0 && genericDepth === 0) parametersStarted = true;
|
|
850
|
+
depth++;
|
|
851
|
+
} else if (text[i] === ")") depth = Math.max(0, depth - 1);
|
|
852
|
+
else if (depth === 0 && typedSignature && text[i] === ":" && prev === ")") returnType = true;
|
|
853
|
+
|
|
854
|
+
// TypeScript generic constraints and return types can contain object types
|
|
855
|
+
// before the body. Consume wrapped members without ending the declaration.
|
|
856
|
+
if (depth === 0 && typedSignature && text[i] === "{" && (prev === "extends"
|
|
857
|
+
|| prev === "readonly" && (genericDepth > 0 || returnType)
|
|
858
|
+
|| genericDepth > 0 && ["keyof", "=>"].includes(prev)
|
|
859
|
+
|| returnType && [":", "|", "&", "<", ",", "?", "[", "=>", "keyof", "type-predicate"].includes(prev))) {
|
|
860
|
+
const end = balancedEnd(text, i, [], rust, cpp, go, js, stringExt);
|
|
861
|
+
|
|
862
|
+
if (end < 0) return { open: -1, end: limit };
|
|
863
|
+
i = end;
|
|
864
|
+
|
|
865
|
+
// The type can cross the initial line bound. Scan the rest of its closing
|
|
866
|
+
// line too: a conditional-type suffix can precede the actual body there.
|
|
867
|
+
if (i > limit) {
|
|
868
|
+
const lineEnd = text.indexOf("\n", i);
|
|
869
|
+
limit = lineEnd < 0 ? text.length : lineEnd;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
prev = "value";
|
|
873
|
+
prevEnd = i;
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
if (depth === 0 && (text[i] === "{" || (text[i] === "[" && prev === "="))) return { open: i, end: i };
|
|
878
|
+
bracket(text[i], i, stack, undefined, prev, js);
|
|
879
|
+
|
|
880
|
+
if (!/\s/.test(text[i])) {
|
|
881
|
+
// A top-level binding annotation introduces a type value, not a body.
|
|
882
|
+
// Its object brace must not end the declaration before the initializer.
|
|
883
|
+
const annotation = !bodyExpected && [".ts", ".tsx", ".mts", ".cts"].includes(stringExt)
|
|
884
|
+
&& text[i] === ":" && stack.length === 0;
|
|
885
|
+
|
|
886
|
+
if (annotation) bindingType = true;
|
|
887
|
+
else if (stack.length === 0 && ["=", ";"].includes(text[i])) bindingType = false;
|
|
888
|
+
prev = annotation ? "=" : punctuationContext(text, i, stack, conditionals, js);
|
|
889
|
+
prevEnd = i + 1;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
i++;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// An unfinished assignment may put its aggregate initializer on the next
|
|
896
|
+
// line. Use the last code token, not a textual '=', so comments cannot
|
|
897
|
+
// make an already completed binding absorb a separate block.
|
|
898
|
+
if (depth === 0 && prev === "=") {
|
|
899
|
+
const next = js ? nextExpressionToken(text, i) : limit + /^\s*/.exec(text.slice(limit))[0].length;
|
|
900
|
+
|
|
901
|
+
if (["{", "["].includes(text[next])) return { open: next, end: i };
|
|
902
|
+
|
|
903
|
+
// Literal and function/class bindings can start after the assignment's newline.
|
|
904
|
+
// Retain the complete expression, without absorbing a later declaration.
|
|
905
|
+
if (js) {
|
|
906
|
+
const expression = consumeLiteral(text, next, [], "=", rust, cpp, go, js, next, stringExt);
|
|
907
|
+
|
|
908
|
+
if (expression) return { open: -1, end: expression.error ? text.length : expression.end };
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Literal consumers can cross the first newline without exposing a bracket.
|
|
913
|
+
// Preserve that extent for bindings initialized by a multiline template/raw string.
|
|
914
|
+
return { open: -1, end: i };
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function unmatchedOpenParen(text, start, limit, rust, cpp, go, js, stringExt) {
|
|
918
|
+
const stack = [];
|
|
919
|
+
const parens = [];
|
|
920
|
+
const conditionals = [];
|
|
921
|
+
let i = start;
|
|
922
|
+
let prev = "";
|
|
923
|
+
let prevEnd = start;
|
|
924
|
+
|
|
925
|
+
while (i < limit) {
|
|
926
|
+
const literal = consumeLiteral(text, i, stack, prev, rust, cpp, go, js, prevEnd, stringExt);
|
|
927
|
+
|
|
928
|
+
if (literal) {
|
|
929
|
+
if (literal.error) return -1;
|
|
930
|
+
i = literal.end;
|
|
931
|
+
prev = literal.prev;
|
|
932
|
+
prevEnd = literal.trivia ? prevEnd : i;
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const j = readIdentifier(text, i);
|
|
937
|
+
|
|
938
|
+
if (j > i) {
|
|
939
|
+
prev = prev === "for" && text.slice(i, j) === "await" ? prev : identifierContext(text.slice(i, j), prev, stack, stringExt);
|
|
940
|
+
i = j;
|
|
941
|
+
prevEnd = i;
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
if (text[i] === "(") parens.push(i);
|
|
946
|
+
else if (text[i] === ")") parens.pop();
|
|
947
|
+
bracket(text[i], i, stack, undefined, prev, js);
|
|
948
|
+
|
|
949
|
+
if (!/\s/.test(text[i])) {
|
|
950
|
+
prev = punctuationContext(text, i, stack, conditionals, js);
|
|
951
|
+
prevEnd = i + 1;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
i++;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
return parens[0] ?? -1;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/** 1-indexed last line of a declaration block; bodyExpected permits a next-line opening brace. */
|
|
961
|
+
export function braceBlockEndLine(text, startLine, ext = "", bodyExpected = false) {
|
|
962
|
+
ext = String(ext ?? "").toLowerCase();
|
|
963
|
+
const rust = ext === ".rs";
|
|
964
|
+
const go = ext === ".go";
|
|
965
|
+
const js = JS_EXT.has(ext);
|
|
966
|
+
const cpp = [".cc", ".cpp", ".h", ".hpp"].includes(ext);
|
|
967
|
+
const start = offsetOfLine(text, startLine);
|
|
968
|
+
|
|
969
|
+
if (start < 0) return startLine;
|
|
970
|
+
const lineEnd = text.indexOf("\n", start);
|
|
971
|
+
const limit = lineEnd < 0 ? text.length : lineEnd;
|
|
972
|
+
const typedSignature = bodyExpected && [".ts", ".tsx", ".mts", ".cts"].includes(ext);
|
|
973
|
+
const declaration = firstDeclarationOpen(text, start, limit, rust, cpp, go, js, typedSignature, ext, bodyExpected);
|
|
974
|
+
let open = declaration.open;
|
|
975
|
+
|
|
976
|
+
// Wrapped signatures and call initializers both leave an unmatched `(`.
|
|
977
|
+
// A binding's call ends at its own `)`, not the next declaration's `{`.
|
|
978
|
+
if (open < 0) {
|
|
979
|
+
const paren = unmatchedOpenParen(text, start, Math.max(limit, declaration.end), rust, cpp, go, js, ext);
|
|
980
|
+
|
|
981
|
+
if (paren >= 0) {
|
|
982
|
+
const end = balancedEnd(text, paren, [], rust, cpp, go, js, ext);
|
|
983
|
+
|
|
984
|
+
// Function expressions and arrow signatures still need their body.
|
|
985
|
+
if (!bodyExpected && end >= 0 && !/^\s*(?:=>|\{)/.test(text.slice(end))) return lineOf(text, end - 1);
|
|
986
|
+
open = firstDeclarationOpen(text, start, text.length, rust, cpp, go, js, typedSignature, ext).open;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
if (open < 0 && bodyExpected) {
|
|
991
|
+
// A completed signature may put its body on the next line. Do not apply
|
|
992
|
+
// this to bindings, where a following block can be a separate statement.
|
|
993
|
+
const nextStart = Math.max(limit, declaration.end);
|
|
994
|
+
const next = js ? nextExpressionToken(text, nextStart) : nextStart + /^\s*/.exec(text.slice(nextStart))[0].length;
|
|
995
|
+
|
|
996
|
+
if (text[next] === "{") open = next;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
if (open < 0) return Math.max(startLine, lineOf(text, declaration.end - 1));
|
|
1000
|
+
const end = balancedEnd(text, open, [], rust, cpp, go, js, ext);
|
|
1001
|
+
|
|
1002
|
+
return end < 0 ? lineOf(text, Math.max(0, text.length - 1)) : lineOf(text, end - 1);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
207
1005
|
const CODE_EXT = new Set([".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx", ".mts", ".cts", ".java", ".kt", ".c", ".cc", ".cpp", ".h", ".hpp", ".cs", ".go", ".rs", ".swift", ".css", ".scss"]);
|
|
208
1006
|
|
|
209
1007
|
/** { ok: true } | { ok: false, message }; message names the problem and line. */
|
|
@@ -223,7 +1021,7 @@ export function quickCheck(text, ext) {
|
|
|
223
1021
|
|
|
224
1022
|
if (!CODE_EXT.has(ext)) return null;
|
|
225
1023
|
const stack = [];
|
|
226
|
-
const r = scan(text, 0, stack, undefined, ext === ".rs");
|
|
1024
|
+
const r = scan(text, 0, stack, undefined, ext === ".rs", [".cc", ".cpp", ".h", ".hpp"].includes(ext), ext === ".go", JS_EXT.has(ext), ext);
|
|
227
1025
|
|
|
228
1026
|
if (r.error) return { ok: false, kind: "balance", message: r.error + " at line " + lineOf(text, r.at) };
|
|
229
1027
|
|