@velarscript/compiler 0.12.0 → 0.13.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/dist/advisory-suppression.d.ts +62 -0
- package/dist/advisory-suppression.d.ts.map +1 -0
- package/dist/advisory-suppression.js +179 -0
- package/dist/advisory-suppression.js.map +1 -0
- package/dist/analyzer.d.ts +405 -17
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +1755 -211
- package/dist/analyzer.js.map +1 -1
- package/dist/ast.d.ts +6 -3
- package/dist/ast.d.ts.map +1 -1
- package/dist/ast.js.map +1 -1
- package/dist/binding-stability.d.ts +22 -0
- package/dist/binding-stability.d.ts.map +1 -0
- package/dist/binding-stability.js +63 -0
- package/dist/binding-stability.js.map +1 -0
- package/dist/collection-lowering-runtime.d.ts +1 -1
- package/dist/collection-lowering-runtime.d.ts.map +1 -1
- package/dist/collection-lowering-runtime.js +183 -56
- package/dist/collection-lowering-runtime.js.map +1 -1
- package/dist/diagnostic.d.ts +34 -0
- package/dist/diagnostic.d.ts.map +1 -1
- package/dist/diagnostic.js +11 -1
- package/dist/diagnostic.js.map +1 -1
- package/dist/embedded-javascript.d.ts +12 -1
- package/dist/embedded-javascript.d.ts.map +1 -1
- package/dist/embedded-javascript.js +111 -2
- package/dist/embedded-javascript.js.map +1 -1
- package/dist/embedded-module.d.ts.map +1 -1
- package/dist/embedded-module.js +22 -0
- package/dist/embedded-module.js.map +1 -1
- package/dist/emitter.d.ts +83 -1
- package/dist/emitter.d.ts.map +1 -1
- package/dist/emitter.js +518 -41
- package/dist/emitter.js.map +1 -1
- package/dist/error-runtime.d.ts +9 -0
- package/dist/error-runtime.d.ts.map +1 -1
- package/dist/error-runtime.js +21 -0
- package/dist/error-runtime.js.map +1 -1
- package/dist/extension.d.ts +5 -2
- package/dist/extension.d.ts.map +1 -1
- package/dist/extension.js +2 -1
- package/dist/extension.js.map +1 -1
- package/dist/formatter.d.ts.map +1 -1
- package/dist/formatter.js +222 -91
- package/dist/formatter.js.map +1 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +43 -5
- package/dist/index.js.map +1 -1
- package/dist/interpolated-string.d.ts.map +1 -1
- package/dist/interpolated-string.js +29 -2
- package/dist/interpolated-string.js.map +1 -1
- package/dist/json-runtime.d.ts.map +1 -1
- package/dist/json-runtime.js +27 -3
- package/dist/json-runtime.js.map +1 -1
- package/dist/lexer.d.ts +217 -1
- package/dist/lexer.d.ts.map +1 -1
- package/dist/lexer.js +799 -37
- package/dist/lexer.js.map +1 -1
- package/dist/limits.d.ts +9 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +9 -0
- package/dist/limits.js.map +1 -1
- package/dist/mechanical-fix.d.ts.map +1 -1
- package/dist/mechanical-fix.js +38 -0
- package/dist/mechanical-fix.js.map +1 -1
- package/dist/parser.d.ts +30 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +137 -21
- package/dist/parser.js.map +1 -1
- package/dist/promise-runtime.d.ts +20 -1
- package/dist/promise-runtime.d.ts.map +1 -1
- package/dist/promise-runtime.js +38 -3
- package/dist/promise-runtime.js.map +1 -1
- package/dist/reactive-bridge-runtime.d.ts.map +1 -1
- package/dist/reactive-bridge-runtime.js +7 -1
- package/dist/reactive-bridge-runtime.js.map +1 -1
- package/dist/runtime-abi.d.ts +21 -1
- package/dist/runtime-abi.d.ts.map +1 -1
- package/dist/runtime-abi.js +23 -3
- package/dist/runtime-abi.js.map +1 -1
- package/dist/source-names.d.ts +26 -0
- package/dist/source-names.d.ts.map +1 -1
- package/dist/source-names.js +49 -5
- package/dist/source-names.js.map +1 -1
- package/dist/source.d.ts.map +1 -1
- package/dist/source.js +13 -0
- package/dist/source.js.map +1 -1
- package/dist/stable-order.d.ts +16 -0
- package/dist/stable-order.d.ts.map +1 -0
- package/dist/stable-order.js +18 -0
- package/dist/stable-order.js.map +1 -0
- package/dist/text-runtime.d.ts.map +1 -1
- package/dist/text-runtime.js +182 -41
- package/dist/text-runtime.js.map +1 -1
- package/dist/token.d.ts +11 -0
- package/dist/token.d.ts.map +1 -1
- package/dist/token.js.map +1 -1
- package/dist/type-registry-runtime.d.ts.map +1 -1
- package/dist/type-registry-runtime.js +8 -1
- package/dist/type-registry-runtime.js.map +1 -1
- package/dist/type-validation-runtime.d.ts.map +1 -1
- package/dist/type-validation-runtime.js +129 -1
- package/dist/type-validation-runtime.js.map +1 -1
- package/dist/types.d.ts +17 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +161 -17
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/lexer.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { scanAdvisorySuppressions } from "./advisory-suppression.js";
|
|
1
2
|
import { CORE_NUMERIC_SUFFIXES } from "./core-vocabulary.js";
|
|
2
|
-
import { diagnostic, mechanicalFix, recoveredDiagnostic } from "./diagnostic.js";
|
|
3
|
+
import { advisory, diagnostic, mechanicalEdits, mechanicalFix, recoveredDiagnostic } from "./diagnostic.js";
|
|
3
4
|
import { scanEmbeddedJavaScriptLiteral } from "./embedded-javascript.js";
|
|
4
5
|
import { findInterpolatedExpressionEnd, scanStringEscape, scanStringLiteral } from "./interpolated-string.js";
|
|
5
6
|
import { webNumericUnitOwner } from "./language-guidance.js";
|
|
7
|
+
import { MAX_LEX_DIAGNOSTICS } from "./limits.js";
|
|
6
8
|
import { forbiddenSourceIdentifiers, isForbiddenPrototypeMember, isSourceIdentifierPart, isSourceIdentifierStart } from "./source-names.js";
|
|
7
9
|
import { span } from "./source.js";
|
|
8
10
|
import { keywordKinds } from "./token.js";
|
|
@@ -29,6 +31,221 @@ const chainContinuationEndKinds = new Set([
|
|
|
29
31
|
"true", "false", "null", "super", "rightParen", "rightBracket", "rightBrace",
|
|
30
32
|
"extensionToken",
|
|
31
33
|
]);
|
|
34
|
+
// D89 A1 reads back the primary expression its comment follows. These are the
|
|
35
|
+
// kinds a primary tail is made of outside brackets — names, literals, member
|
|
36
|
+
// steps, and the two postfix marks; brackets themselves are matched by depth.
|
|
37
|
+
// Anything else ends the walk, which is what keeps the advisory's rewrite from
|
|
38
|
+
// reaching across an operator that binds looser than `//` does.
|
|
39
|
+
//
|
|
40
|
+
// A literal is here for its interior reading, not its final one: `"abc".size`
|
|
41
|
+
// and `f"{a}".size` are dividends whose walk passes back through the literal
|
|
42
|
+
// on the way to the name that ends them. What may *end* a dividend is the
|
|
43
|
+
// narrower question `floorDivisionDividendEndKinds` answers.
|
|
44
|
+
const primaryTailKinds = new Set([
|
|
45
|
+
"identifier", "number", "unitNumber", "string", "fstring", "extensionToken",
|
|
46
|
+
"true", "false", "null", "super", "dot", "optionalDot", "bang",
|
|
47
|
+
]);
|
|
48
|
+
// D90: the token a floor-division mistake can actually stand on. A1 used to
|
|
49
|
+
// borrow `chainContinuationEndKinds`, which answers a different question — a
|
|
50
|
+
// string, an f-string, `true`, `false`, `null`, `super` and a record's `}` all
|
|
51
|
+
// end an expression that a leading-dot line may continue, and none of them can
|
|
52
|
+
// be divided, so `const s = "x" // 2` drew an advisory suggesting
|
|
53
|
+
// `("x" / 2).floor()`, which the author cannot act on. D89 admits an advisory
|
|
54
|
+
// only when its trigger narrows to near-zero false positives, which makes that
|
|
55
|
+
// disqualifying. What remains: a name, a plain numeric literal, `)` closing a
|
|
56
|
+
// call or a group, `]` closing an index (`xs[0] // 2`), and `!` closing a
|
|
57
|
+
// required-value unwrap (`total! // 2`).
|
|
58
|
+
//
|
|
59
|
+
// A unit number is not here either. `10s` is a Duration, `(10s / 2).floor()`
|
|
60
|
+
// does not typecheck (Duration has no `floor`), and nobody reaches for Python's
|
|
61
|
+
// floor division on a duration literal — so the rewrite would be one the author
|
|
62
|
+
// cannot use, which is the same disqualification the string tail carried.
|
|
63
|
+
const floorDivisionDividendEndKinds = new Set([
|
|
64
|
+
"identifier", "number", "rightParen", "rightBracket", "bang",
|
|
65
|
+
]);
|
|
66
|
+
// The tokens that end one logical line's token run, and the words a class
|
|
67
|
+
// header may carry ahead of `class`. Both are read when a block opens, to
|
|
68
|
+
// decide whether the block being entered is a class body.
|
|
69
|
+
const lineBoundaryKinds = new Set(["newline", "indent", "dedent"]);
|
|
70
|
+
const classHeaderModifierKinds = new Set(["export", "abstract"]);
|
|
71
|
+
/** How far back the receiver-parameter walk reads before giving the name up. */
|
|
72
|
+
const RECEIVER_PARAMETER_SCAN_LIMIT = 4096;
|
|
73
|
+
// D90 (compiler-front-14): the words that open a declaration or a statement.
|
|
74
|
+
// A physical line inside an open bracket that begins with one of these, at or
|
|
75
|
+
// below the indentation of the line that opened the bracket, is the evidence
|
|
76
|
+
// that the bracket was never closed rather than still being filled in.
|
|
77
|
+
const statementHeadWords = new Set([
|
|
78
|
+
"export", "def", "class", "const", "let", "enum", "import", "return",
|
|
79
|
+
"if", "for", "while", "match", "type",
|
|
80
|
+
]);
|
|
81
|
+
// What may stand after one of those words when the line is *not* a statement
|
|
82
|
+
// head: a record key's ':', the separators and closers that finish a read of a
|
|
83
|
+
// binding named `type` or `match`, the '=' of a named argument written
|
|
84
|
+
// `type=1`, and the '.' of a member step. None of them can follow a real
|
|
85
|
+
// declaration keyword, so withholding recovery on them refuses nothing.
|
|
86
|
+
const statementReadFollowers = new Set([":", ",", ")", "]", "}", "=", "."]);
|
|
87
|
+
/**
|
|
88
|
+
* D89 A1's comment body, split into what Python's `//` would have divided by
|
|
89
|
+
* and what it would have gone on to do. `//` binds as tightly as `*`, so the
|
|
90
|
+
* Python author who wrote `total // 2 + 3` divided by 2 and *then* added 3.
|
|
91
|
+
* Both readings that came before this were wrong: quoting the body verbatim
|
|
92
|
+
* gave `(total / 2 + 3).floor()`, and wrapping the whole body gave
|
|
93
|
+
* `(total / (2 + 3)).floor()`, which for `total = 10` answers 2 where Python
|
|
94
|
+
* answers 8. The divisor is the leading primary alone — a number or an
|
|
95
|
+
* already-parenthesised group — and the tail is re-emitted after `.floor()`,
|
|
96
|
+
* where it binds exactly as it did after Python's `//`.
|
|
97
|
+
*
|
|
98
|
+
* `null` withholds the advisory. D89's admission bar, item 4, requires that a
|
|
99
|
+
* zero-cost rewrite exist and that the advisory name that one unambiguous
|
|
100
|
+
* spelling, which makes an advisory that cannot name a correct rewrite
|
|
101
|
+
* inadmissible. A body this cannot translate by a single substitution therefore
|
|
102
|
+
* reports nothing at all: unbalanced parentheses (`// 2)` is as
|
|
103
|
+
* likely a stray keystroke as a divisor), a body that does not open with a
|
|
104
|
+
* primary (`// 2 3` does not parse either way), a tail that is not one
|
|
105
|
+
* arithmetic step (`+`, `-`, `*` and then something), and a second `/` or `%`
|
|
106
|
+
* anywhere in the tail — a second floor division or a modulo cannot be
|
|
107
|
+
* expressed by one substitution, and `//` inside the suggested text would open
|
|
108
|
+
* a comment in the very line it is telling the author to write. Silence is the
|
|
109
|
+
* safe half of that trade; a wrong suggestion is a new defect.
|
|
110
|
+
*/
|
|
111
|
+
function floorDivisionRewrite(body) {
|
|
112
|
+
let depth = 0;
|
|
113
|
+
let primaryEnd = -1;
|
|
114
|
+
for (let index = 0; index < body.length; index += 1) {
|
|
115
|
+
if (body[index] === "(")
|
|
116
|
+
depth += 1;
|
|
117
|
+
else if (body[index] === ")") {
|
|
118
|
+
depth -= 1;
|
|
119
|
+
if (depth < 0)
|
|
120
|
+
return null;
|
|
121
|
+
if (depth === 0 && primaryEnd < 0 && body.startsWith("("))
|
|
122
|
+
primaryEnd = index + 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (depth !== 0)
|
|
126
|
+
return null;
|
|
127
|
+
if (!body.startsWith("(")) {
|
|
128
|
+
const primary = /^[0-9]+(?:\.[0-9]+)?/u.exec(body);
|
|
129
|
+
if (primary === null)
|
|
130
|
+
return null;
|
|
131
|
+
primaryEnd = primary[0].length;
|
|
132
|
+
}
|
|
133
|
+
const divisor = body.slice(0, primaryEnd);
|
|
134
|
+
if (divisor.includes("//"))
|
|
135
|
+
return null;
|
|
136
|
+
const rest = body.slice(primaryEnd).trim();
|
|
137
|
+
if (rest === "")
|
|
138
|
+
return { divisor, tail: "" };
|
|
139
|
+
if (!/^[+\-*]\s*\S/u.test(rest) || rest.includes("/") || rest.includes("%"))
|
|
140
|
+
return null;
|
|
141
|
+
return { divisor, tail: ` ${rest[0]} ${rest.slice(1).trim().replace(/\s+/gu, " ")}` };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* D89 A5/A6: the interpolation bodies whose rewrite is registered as a
|
|
145
|
+
* mechanical fix. D38 §48 admits a fix only where no judgment is involved, and
|
|
146
|
+
* the judgment-free core of "delete the `$`" is a body that is already a Vel
|
|
147
|
+
* expression: a dotted name path reads identically in JavaScript and in an
|
|
148
|
+
* `f` string, so the rewrite is a spelling change. Anything wider — a call, an
|
|
149
|
+
* operator, a JavaScript-only form like `a ?? b.x()` — might not compile once
|
|
150
|
+
* it becomes an interpolation, and a registered fix that hands back a new
|
|
151
|
+
* diagnostic is a guess, not a fix. Those bodies keep the advisory and lose
|
|
152
|
+
* only the one-click edit.
|
|
153
|
+
*/
|
|
154
|
+
const interpolationPathBody = /^[\p{L}_][\p{L}\p{N}_]*(?:\.[\p{L}_][\p{L}\p{N}_]*)*$/u;
|
|
155
|
+
/**
|
|
156
|
+
* D89 A5/A6: reads the `${...}` occurrences of one string literal's content.
|
|
157
|
+
* The walk mirrors `diagnoseStringContents` so the two never disagree about
|
|
158
|
+
* what a brace means: escapes are skipped in non-raw text (`"\u{E9}"` carries
|
|
159
|
+
* a brace that belongs to the escape), and in an interpolated string a `{{`
|
|
160
|
+
* pair and a real `{...}` interpolation are stepped over rather than counted,
|
|
161
|
+
* because there they already mean what the author asked for. Brace matching
|
|
162
|
+
* inside a `${...}` body is by depth, which is the same reading JavaScript
|
|
163
|
+
* gives the template it came from.
|
|
164
|
+
*/
|
|
165
|
+
function templateInterpolationScan(content, syntax) {
|
|
166
|
+
const occurrences = [];
|
|
167
|
+
let allWellFormed = true;
|
|
168
|
+
let bareBrace = false;
|
|
169
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
170
|
+
const character = content[index];
|
|
171
|
+
if (!syntax.raw && character === "\\") {
|
|
172
|
+
index = scanStringEscape(content, index).end - 1;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (character !== "{") {
|
|
176
|
+
if (!syntax.interpolated && character === "}")
|
|
177
|
+
bareBrace = true;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (content[index - 1] === "$" && !(syntax.interpolated && content[index + 1] === "{")) {
|
|
181
|
+
// In an interpolated string `${{x}}` is a literal `$` ahead of a real
|
|
182
|
+
// interpolation, so the `$` there is not holding anything back and the
|
|
183
|
+
// next iteration reads the braces the way the lexer does.
|
|
184
|
+
const close = matchInterpolationClose(content, index + 1);
|
|
185
|
+
if (close < 0 || content.slice(index + 1, close).trim() === "") {
|
|
186
|
+
allWellFormed = false;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
occurrences.push({ dollar: index - 1, close, body: content.slice(index + 1, close) });
|
|
190
|
+
index = close;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (!syntax.interpolated) {
|
|
194
|
+
bareBrace = true;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (content[index + 1] === "{") {
|
|
198
|
+
index += 1;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const close = findInterpolatedExpressionEnd(content, index + 1);
|
|
202
|
+
if (close < 0)
|
|
203
|
+
break;
|
|
204
|
+
index = close;
|
|
205
|
+
}
|
|
206
|
+
return { occurrences, allWellFormed, bareBrace };
|
|
207
|
+
}
|
|
208
|
+
/** The index of the `}` closing the brace at `open - 1`, matched by depth, or -1. */
|
|
209
|
+
function matchInterpolationClose(content, open) {
|
|
210
|
+
let depth = 1;
|
|
211
|
+
for (let index = open; index < content.length; index += 1) {
|
|
212
|
+
if (content[index] === "{")
|
|
213
|
+
depth += 1;
|
|
214
|
+
else if (content[index] === "}") {
|
|
215
|
+
depth -= 1;
|
|
216
|
+
if (depth === 0)
|
|
217
|
+
return index;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return -1;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* The diagnostics of one lex, capped. Pathological input reports once per
|
|
224
|
+
* character — a minified JavaScript file pasted into a `.vel` buffer that the
|
|
225
|
+
* language server re-lexes on every keystroke — and millions of retained
|
|
226
|
+
* reports help nobody. The cap never drops the tail silently: its last slot
|
|
227
|
+
* says that it closed, so a real error can never hide behind the truncation.
|
|
228
|
+
*/
|
|
229
|
+
class DiagnosticLog {
|
|
230
|
+
entries = [];
|
|
231
|
+
closed = false;
|
|
232
|
+
push(...reports) {
|
|
233
|
+
for (const report of reports) {
|
|
234
|
+
if (this.entries.length < MAX_LEX_DIAGNOSTICS - 1) {
|
|
235
|
+
this.entries.push(report);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (this.closed)
|
|
239
|
+
return;
|
|
240
|
+
this.closed = true;
|
|
241
|
+
this.entries.push(diagnostic("VEL1013", `This module reported ${MAX_LEX_DIAGNOSTICS - 1} lexical errors, which is as many as VelarScript reports at once; fix these and compile again to see the rest`, report.span));
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
get reports() {
|
|
246
|
+
return this.entries;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
32
249
|
export class Lexer {
|
|
33
250
|
text;
|
|
34
251
|
extensionForbiddenIdentifiers = new Map();
|
|
@@ -40,12 +257,39 @@ export class Lexer {
|
|
|
40
257
|
// extension republishing them.
|
|
41
258
|
numericSuffixes = new Set(CORE_NUMERIC_SUFFIXES);
|
|
42
259
|
tokens = [];
|
|
43
|
-
diagnostics =
|
|
260
|
+
diagnostics = new DiagnosticLog();
|
|
261
|
+
advisories = [];
|
|
262
|
+
suppressions = [];
|
|
44
263
|
diagnosedBidirectionalOffsets = new Set();
|
|
45
264
|
indentStack = [0];
|
|
265
|
+
// D90 (compiler-front-9): whether each open block is a class body, kept in
|
|
266
|
+
// step with `indentStack`. A member may be spelled `with` or `int`; a binding
|
|
267
|
+
// may not, and `def with(...)` at module scope would emit `function with`,
|
|
268
|
+
// which is not JavaScript. Only the enclosing block tells the two apart.
|
|
269
|
+
classBodyStack = [false];
|
|
270
|
+
// D90 (compiler-front-14): the brackets still open, with the indentation of
|
|
271
|
+
// the physical line each one was opened on.
|
|
272
|
+
openBrackets = [];
|
|
46
273
|
index = 0;
|
|
47
274
|
atLineStart = true;
|
|
275
|
+
// A physical line began while brackets were open, where no newline token is
|
|
276
|
+
// emitted and no indentation is read. The unclosed-bracket recovery is the
|
|
277
|
+
// only thing that looks at those lines.
|
|
278
|
+
bracketLineStart = false;
|
|
48
279
|
nesting = 0;
|
|
280
|
+
// The indentation of the physical line that opened the current logical line.
|
|
281
|
+
// A leading-dot continuation is measured against this rather than against the
|
|
282
|
+
// previous physical line, so every line of one chain answers to one rule.
|
|
283
|
+
logicalLineIndent = 0;
|
|
284
|
+
// The forward line scan `lineStart` and `lineEnd` share; see `lineStart`.
|
|
285
|
+
scannedLineStart = 0;
|
|
286
|
+
scannedTo = 0;
|
|
287
|
+
cachedLineEndFrom = -1;
|
|
288
|
+
cachedLineEnd = -1;
|
|
289
|
+
// The end of the run of semicolons and blanks a trailing-semicolon fix last
|
|
290
|
+
// measured. Every semicolon in one run reaches the same offset, so the run is
|
|
291
|
+
// walked once rather than once per semicolon.
|
|
292
|
+
semicolonRunEnd = -1;
|
|
49
293
|
// A bracket fragment is an expression lexed inside an enclosing bracket
|
|
50
294
|
// context, such as an extension-owned bracket interpolation: newlines are insignificant
|
|
51
295
|
// and physical-line indentation never opens or closes blocks, exactly as
|
|
@@ -80,6 +324,10 @@ export class Lexer {
|
|
|
80
324
|
this.index = this.text.length;
|
|
81
325
|
break;
|
|
82
326
|
}
|
|
327
|
+
if (this.bracketLineStart) {
|
|
328
|
+
this.bracketLineStart = false;
|
|
329
|
+
this.recoverUnclosedBrackets();
|
|
330
|
+
}
|
|
83
331
|
if (this.atLineStart && this.nesting === 0 && !this.bracketFragment) {
|
|
84
332
|
this.readIndentation();
|
|
85
333
|
}
|
|
@@ -143,27 +391,27 @@ export class Lexer {
|
|
|
143
391
|
switch (character) {
|
|
144
392
|
case "(":
|
|
145
393
|
this.simple("leftParen", start, 1);
|
|
146
|
-
this.
|
|
394
|
+
this.openBracket(start);
|
|
147
395
|
break;
|
|
148
396
|
case ")":
|
|
149
397
|
this.simple("rightParen", start, 1);
|
|
150
|
-
this.
|
|
398
|
+
this.closeBracket();
|
|
151
399
|
break;
|
|
152
400
|
case "[":
|
|
153
401
|
this.simple("leftBracket", start, 1);
|
|
154
|
-
this.
|
|
402
|
+
this.openBracket(start);
|
|
155
403
|
break;
|
|
156
404
|
case "]":
|
|
157
405
|
this.simple("rightBracket", start, 1);
|
|
158
|
-
this.
|
|
406
|
+
this.closeBracket();
|
|
159
407
|
break;
|
|
160
408
|
case "{":
|
|
161
409
|
this.simple("leftBrace", start, 1);
|
|
162
|
-
this.
|
|
410
|
+
this.openBracket(start);
|
|
163
411
|
break;
|
|
164
412
|
case "}":
|
|
165
413
|
this.simple("rightBrace", start, 1);
|
|
166
|
-
this.
|
|
414
|
+
this.closeBracket();
|
|
167
415
|
break;
|
|
168
416
|
case ":":
|
|
169
417
|
if (this.peek(1) === "=") {
|
|
@@ -336,7 +584,7 @@ export class Lexer {
|
|
|
336
584
|
this.tokens.push({ kind: "dedent", value: "", span: span(this.index, this.index) });
|
|
337
585
|
}
|
|
338
586
|
this.tokens.push({ kind: "eof", value: "", span: span(this.index, this.index) });
|
|
339
|
-
return { tokens: this.tokens, diagnostics: this.diagnostics };
|
|
587
|
+
return { tokens: this.tokens, diagnostics: this.diagnostics.reports, advisories: this.advisories, suppressions: this.suppressions };
|
|
340
588
|
}
|
|
341
589
|
readIndentation() {
|
|
342
590
|
const start = this.index;
|
|
@@ -364,12 +612,20 @@ export class Lexer {
|
|
|
364
612
|
// A leading-dot line continues the previous logical line: the newline
|
|
365
613
|
// tokens that ended it are withdrawn and this line's indentation does not
|
|
366
614
|
// open or close a block, so '.filter(...)' chains span physical lines.
|
|
367
|
-
|
|
368
|
-
|
|
615
|
+
const dotWidth = this.leadingDotWidth();
|
|
616
|
+
if (dotWidth > 0) {
|
|
617
|
+
if (this.isChainContinuation(width)) {
|
|
369
618
|
this.tokens.pop();
|
|
370
|
-
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
// The line looked like a continuation and is not one, so it is read as
|
|
622
|
+
// its own statement — which it cannot be, because no statement begins
|
|
623
|
+
// with a member step. Saying so here is the whole point of tightening
|
|
624
|
+
// the rule: the alternative is the silent reattachment this replaces.
|
|
625
|
+
this.diagnostics.push(diagnostic("VEL1004", `A line beginning with '${dotWidth === 2 ? "?." : "."}' continues the line above it, so it must follow that line directly and be indented past the statement it continues`, span(start, this.index + dotWidth)));
|
|
371
626
|
}
|
|
372
627
|
const current = this.indentStack.at(-1) ?? 0;
|
|
628
|
+
this.logicalLineIndent = width;
|
|
373
629
|
if (width > current) {
|
|
374
630
|
if (this.indentStack.length > MAX_NESTING) {
|
|
375
631
|
this.diagnostics.push(diagnostic("VEL1006", `Indentation nesting cannot exceed ${MAX_NESTING} levels`, span(start, this.index)));
|
|
@@ -377,12 +633,14 @@ export class Lexer {
|
|
|
377
633
|
return;
|
|
378
634
|
}
|
|
379
635
|
this.indentStack.push(width);
|
|
636
|
+
this.classBodyStack.push(this.opensClassBody());
|
|
380
637
|
this.tokens.push({ kind: "indent", value: "", span: span(start, this.index) });
|
|
381
638
|
return;
|
|
382
639
|
}
|
|
383
640
|
if (width < current) {
|
|
384
641
|
while (this.indentStack.length > 1 && width < (this.indentStack.at(-1) ?? 0)) {
|
|
385
642
|
this.indentStack.pop();
|
|
643
|
+
this.classBodyStack.pop();
|
|
386
644
|
this.tokens.push({ kind: "dedent", value: "", span: span(start, this.index) });
|
|
387
645
|
}
|
|
388
646
|
if (width !== (this.indentStack.at(-1) ?? 0)) {
|
|
@@ -390,17 +648,57 @@ export class Lexer {
|
|
|
390
648
|
}
|
|
391
649
|
}
|
|
392
650
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
651
|
+
/** The width of a leading member step, or 0 where the line does not open with one. */
|
|
652
|
+
leadingDotWidth() {
|
|
653
|
+
const width = this.peek() === "." ? 1 : this.peek() === "?" && this.peek(1) === "." ? 2 : 0;
|
|
654
|
+
// '.5' is a decimal literal with its leading digit missing, not a member
|
|
655
|
+
// step, and it carries its own diagnostic.
|
|
656
|
+
return width > 0 && this.isIdentifierStart(this.peek(width)) ? width : 0;
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Whether the leading-dot line at `width` joins the line above it. Two
|
|
660
|
+
* conditions, and the file's own contract has always claimed both:
|
|
661
|
+
*
|
|
662
|
+
* - It is the *next* line. The backward walk used to skip an unbounded run of
|
|
663
|
+
* `newline` tokens, so a chain joined a value that appeared any number of
|
|
664
|
+
* blank lines and whole-line comments earlier — the case the header comment
|
|
665
|
+
* says "never join accidentally" (D90, compiler-front-10). One `newline`
|
|
666
|
+
* token is the line that ended the statement; a second is a blank or
|
|
667
|
+
* comment line, and the statement ended there.
|
|
668
|
+
* - It is indented past the statement it continues. The charter called the
|
|
669
|
+
* deeper indentation canonical and nothing enforced it, so a column-0
|
|
670
|
+
* `.sorted()` dedented out of a function body and silently became part of
|
|
671
|
+
* it. `logicalLineIndent` is the *statement's* indentation rather than the
|
|
672
|
+
* previous physical line's, so every line of one chain answers to one rule.
|
|
673
|
+
*/
|
|
674
|
+
isChainContinuation(width) {
|
|
675
|
+
const index = this.tokens.length - 1;
|
|
676
|
+
if (this.tokens[index]?.kind !== "newline")
|
|
677
|
+
return false;
|
|
678
|
+
if (this.tokens[index - 1]?.kind === "newline")
|
|
396
679
|
return false;
|
|
680
|
+
const previous = this.tokens[index - 1];
|
|
681
|
+
if (previous === undefined || !chainContinuationEndKinds.has(previous.kind))
|
|
682
|
+
return false;
|
|
683
|
+
return width > this.logicalLineIndent;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Whether the logical line that just ended opens a class body. Read by the
|
|
687
|
+
* member-name exemption: `def with(...)` declares a member here and a binding
|
|
688
|
+
* anywhere else, and only the enclosing block distinguishes them.
|
|
689
|
+
*/
|
|
690
|
+
opensClassBody() {
|
|
397
691
|
let index = this.tokens.length - 1;
|
|
398
|
-
|
|
692
|
+
while (index >= 0 && this.tokens[index].kind === "newline")
|
|
693
|
+
index -= 1;
|
|
694
|
+
if (this.tokens[index]?.kind !== "colon")
|
|
399
695
|
return false;
|
|
400
|
-
while (this.tokens[index]
|
|
696
|
+
while (index >= 0 && !lineBoundaryKinds.has(this.tokens[index].kind))
|
|
401
697
|
index -= 1;
|
|
402
|
-
|
|
403
|
-
|
|
698
|
+
let head = index + 1;
|
|
699
|
+
while (classHeaderModifierKinds.has(this.tokens[head]?.kind ?? "eof"))
|
|
700
|
+
head += 1;
|
|
701
|
+
return this.tokens[head]?.kind === "class";
|
|
404
702
|
}
|
|
405
703
|
readNewline() {
|
|
406
704
|
const start = this.index;
|
|
@@ -414,11 +712,238 @@ export class Lexer {
|
|
|
414
712
|
this.tokens.push({ kind: "newline", value: "", span: span(start, this.index) });
|
|
415
713
|
this.atLineStart = true;
|
|
416
714
|
}
|
|
715
|
+
else if (!this.bracketFragment) {
|
|
716
|
+
this.bracketLineStart = true;
|
|
717
|
+
}
|
|
417
718
|
}
|
|
719
|
+
/**
|
|
720
|
+
* D90 (compiler-front-14): an unclosed bracket used to swallow the rest of
|
|
721
|
+
* the module. While `nesting > 0` no newline token is emitted and no
|
|
722
|
+
* indentation is read, so one mistyped `(` turned every declaration below it
|
|
723
|
+
* into a continuation of one logical line — a file of fifty exports became
|
|
724
|
+
* one symbol, and none of the reported diagnostics named the bracket.
|
|
725
|
+
*
|
|
726
|
+
* Recovery is narrow because line breaks inside brackets really are
|
|
727
|
+
* insignificant: `compute(` with its arguments at column 0 is legal and must
|
|
728
|
+
* keep compiling, so indentation alone decides nothing. What decides is a
|
|
729
|
+
* physical line that begins a declaration or a statement — a word no
|
|
730
|
+
* bracketed expression can continue with — at or below the indentation of the
|
|
731
|
+
* line that opened the bracket. An indentation-significant language can read
|
|
732
|
+
* on from there; a brace language cannot.
|
|
733
|
+
*/
|
|
734
|
+
recoverUnclosedBrackets() {
|
|
735
|
+
const opening = this.openBrackets[0];
|
|
736
|
+
if (opening === undefined)
|
|
737
|
+
return;
|
|
738
|
+
let cursor = this.index;
|
|
739
|
+
let width = 0;
|
|
740
|
+
while (cursor < this.text.length && (this.text[cursor] === " " || this.text[cursor] === "\t")) {
|
|
741
|
+
width += this.text[cursor] === "\t" ? 4 : 1;
|
|
742
|
+
cursor += 1;
|
|
743
|
+
}
|
|
744
|
+
// An arrow body's statements are the parser's to report: VEL2030 names the
|
|
745
|
+
// remedy, and a '}' one line down means the bracket was never unclosed in
|
|
746
|
+
// the first place. Recovering here would trade a teaching diagnostic for a
|
|
747
|
+
// structural one.
|
|
748
|
+
if (opening.arrowBody)
|
|
749
|
+
return;
|
|
750
|
+
if (width > opening.lineIndent)
|
|
751
|
+
return;
|
|
752
|
+
let end = cursor;
|
|
753
|
+
while (end < this.text.length && this.isIdentifierPart(this.text[end] ?? ""))
|
|
754
|
+
end += 1;
|
|
755
|
+
if (!statementHeadWords.has(this.text.slice(cursor, end)))
|
|
756
|
+
return;
|
|
757
|
+
// Two of those words are ordinary names as well — `type` and `match` are
|
|
758
|
+
// contextual keywords the charter keeps available (section 3) — and any of
|
|
759
|
+
// them may spell a record key. What follows the word tells a declaration
|
|
760
|
+
// from a read: a declaration continues with its subject (`type Name =`,
|
|
761
|
+
// `match value:`, `const x`), while a read is finished, so the next thing
|
|
762
|
+
// it can carry is the punctuation that separates or closes it. A statement
|
|
763
|
+
// head is never followed by one of those, which is what makes withholding
|
|
764
|
+
// on them cost nothing.
|
|
765
|
+
if (statementReadFollowers.has(this.text[this.skipHorizontalWhitespace(end)] ?? ""))
|
|
766
|
+
return;
|
|
767
|
+
this.diagnostics.push(diagnostic("VEL1003", `Unclosed '${opening.text}'; the line below it starts a new declaration, so VelarScript reads on from there rather than to the end of the module`, opening.span));
|
|
768
|
+
this.openBrackets.length = 0;
|
|
769
|
+
this.nesting = 0;
|
|
770
|
+
this.tokens.push({ kind: "newline", value: "", span: span(this.index, this.index) });
|
|
771
|
+
this.atLineStart = true;
|
|
772
|
+
}
|
|
773
|
+
openBracket(start) {
|
|
774
|
+
this.nesting += 1;
|
|
775
|
+
if (this.bracketFragment)
|
|
776
|
+
return;
|
|
777
|
+
const lineStart = this.lineStart(start);
|
|
778
|
+
let width = 0;
|
|
779
|
+
for (let cursor = lineStart; cursor < start; cursor += 1) {
|
|
780
|
+
if (this.text[cursor] === " ")
|
|
781
|
+
width += 1;
|
|
782
|
+
else if (this.text[cursor] === "\t")
|
|
783
|
+
width += 4;
|
|
784
|
+
else
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
// A '{' straight after '=>' is an arrow body, and a statement inside one is
|
|
788
|
+
// exactly what VEL2030 exists to report — a message that names the fix
|
|
789
|
+
// ("move multi-statement logic into a named 'def'") where VEL1003 only
|
|
790
|
+
// reports structure. Recovery yields to it; see recoverUnclosedBrackets.
|
|
791
|
+
// The '{' token is pushed by the caller before this runs, so the arrow — if
|
|
792
|
+
// there is one — sits one further back.
|
|
793
|
+
const arrowBody = (this.text[start] ?? "") === "{" && this.tokens[this.tokens.length - 2]?.kind === "fatArrow";
|
|
794
|
+
this.openBrackets.push({ span: span(start, start + 1), text: this.text[start] ?? "(", lineIndent: width, arrowBody });
|
|
795
|
+
}
|
|
796
|
+
closeBracket() {
|
|
797
|
+
this.nesting = Math.max(0, this.nesting - 1);
|
|
798
|
+
this.openBrackets.pop();
|
|
799
|
+
}
|
|
800
|
+
// D89: a line comment carries no token, but it may carry a `velar-allow`
|
|
801
|
+
// suppression, so its text is read once here. A malformed suppression is a
|
|
802
|
+
// diagnostic rather than an advisory: an unreasoned one may not pass.
|
|
418
803
|
readComment() {
|
|
804
|
+
const start = this.index;
|
|
805
|
+
const bodyStart = this.text.startsWith("//", start) ? start + 2 : start;
|
|
419
806
|
while (!this.isAtEnd() && this.peek() !== "\n" && this.peek() !== "\r") {
|
|
420
807
|
this.advance();
|
|
421
808
|
}
|
|
809
|
+
const scanned = scanAdvisorySuppressions(this.text, start, bodyStart, this.index);
|
|
810
|
+
this.suppressions.push(...scanned.suppressions);
|
|
811
|
+
this.diagnostics.push(...scanned.diagnostics);
|
|
812
|
+
if (bodyStart !== start)
|
|
813
|
+
this.adviseFloorDivisionComment(start, bodyStart, scanned.contentEnd);
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* D89 A1: `//` is VelarScript's only comment spelling, so a Python author's
|
|
817
|
+
* floor division reads as a finished line followed by a comment and the
|
|
818
|
+
* compiler has nothing to object to. `#` cannot take the comment role over:
|
|
819
|
+
* of the branches the `#` dispatch reads (`readJavaScriptPrivateIdentifier`,
|
|
820
|
+
* `readHexColor`, `readHashComment`), `readHexColor` carries bare
|
|
821
|
+
* hexadecimal colors — a hot path in a language with `look:` — and a `#`
|
|
822
|
+
* that opened a comment would swallow `#ff0000` and the rest of its line
|
|
823
|
+
* instead of guiding it to the quoted spelling. With no comment spelling
|
|
824
|
+
* left to give up, an advisory is the only remaining move.
|
|
825
|
+
*
|
|
826
|
+
* The trigger is narrow on both sides. D89 asks for a syntactically complete
|
|
827
|
+
* expression or assignment ahead of the `//`, which here means three things:
|
|
828
|
+
* the line carries code, every bracket *this physical line* opened is closed
|
|
829
|
+
* before the comment (an unclosed one leaves the line unfinished, so
|
|
830
|
+
* `print( // 2` is silent), and the last token can end a dividend. The
|
|
831
|
+
* bracket test counts opens and closes on the line itself rather than reading
|
|
832
|
+
* `nesting`: a bracket opened on an earlier line and closed on a later one
|
|
833
|
+
* leaves the text before `//` complete, so `print(` / ` total // 2` / `)`
|
|
834
|
+
* is exactly the mistake this advisory exists for and used to compile in
|
|
835
|
+
* silence (D90). The comment's own text — with any `velar-allow` clause
|
|
836
|
+
* removed, which is what `contentEnd` is for — must then be a bare arithmetic
|
|
837
|
+
* body carrying a digit and no letter anywhere, so `// TODO`,
|
|
838
|
+
* `// 2. then handle X`, a bare `//`, and a whole-line comment are all
|
|
839
|
+
* silent.
|
|
840
|
+
*
|
|
841
|
+
* A bracket fragment is exempt because its lexer holds the fragment's text
|
|
842
|
+
* rather than the module's: `lineStart` there answers with the fragment's own
|
|
843
|
+
* beginning, and "the rest of this line is a comment" would be a claim about
|
|
844
|
+
* an interpolation rather than about a physical line.
|
|
845
|
+
*
|
|
846
|
+
* No mechanical fix is registered: deciding that the comment really was a
|
|
847
|
+
* divisor is the judgment D38 §48 keeps out of the fix registry.
|
|
848
|
+
*/
|
|
849
|
+
adviseFloorDivisionComment(start, bodyStart, contentEnd) {
|
|
850
|
+
if (this.bracketFragment)
|
|
851
|
+
return;
|
|
852
|
+
const lineStart = this.lineStart(start);
|
|
853
|
+
if (this.text.slice(lineStart, start).trim() === "")
|
|
854
|
+
return;
|
|
855
|
+
const previous = this.tokens.at(-1);
|
|
856
|
+
if (!previous || previous.span.end <= lineStart || !floorDivisionDividendEndKinds.has(previous.kind))
|
|
857
|
+
return;
|
|
858
|
+
if (this.hasBracketOpenedOnLine(lineStart))
|
|
859
|
+
return;
|
|
860
|
+
const body = this.text.slice(bodyStart, contentEnd).trim();
|
|
861
|
+
if (!/[0-9]/u.test(body) || !/^[0-9\s+\-*/%()]+$/u.test(body))
|
|
862
|
+
return;
|
|
863
|
+
const division = floorDivisionRewrite(body);
|
|
864
|
+
if (division === null)
|
|
865
|
+
return;
|
|
866
|
+
const dividend = this.dividendBeforeComment(lineStart, start);
|
|
867
|
+
if (!dividend)
|
|
868
|
+
return;
|
|
869
|
+
const rewrite = `(${dividend.value} / ${division.divisor}).floor()${division.tail}`;
|
|
870
|
+
this.advisories.push(advisory("A1", dividend.target === null
|
|
871
|
+
? `'//' is VelarScript's comment spelling, so the rest of this line is a comment and nothing divides '${dividend.value}'; write '${rewrite}' for Python's floor division`
|
|
872
|
+
: `'//' is VelarScript's comment spelling, so '${dividend.target}' receives '${dividend.value}' and the rest of this line is a comment; write '${rewrite}' for Python's floor division`, span(start, contentEnd)));
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Whether a bracket opened on this physical line is still open at the
|
|
876
|
+
* comment. This is the "syntactically complete" half of A1's trigger that
|
|
877
|
+
* `nesting` was standing in for: `nesting` also counts a bracket opened three
|
|
878
|
+
* lines up, whose text before the `//` is complete all the same.
|
|
879
|
+
*/
|
|
880
|
+
hasBracketOpenedOnLine(lineStart) {
|
|
881
|
+
const innermost = this.openBrackets.at(-1);
|
|
882
|
+
return innermost !== undefined && innermost.span.start >= lineStart;
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* What Python's `//` would have divided: the primary expression the comment
|
|
886
|
+
* follows, read back from the source so the advisory quotes the author's own
|
|
887
|
+
* spelling. The walk stops at the first operator or keyword outside brackets,
|
|
888
|
+
* because `//` binds as tightly as `*` — in `a + b // 2` the dividend is `b`,
|
|
889
|
+
* and naming `a + b` would hand back a rewrite that changes the result.
|
|
890
|
+
*
|
|
891
|
+
* A leading unary sign belongs to the dividend. Python's `-7 // 2` is -4, so
|
|
892
|
+
* quoting `7` and suggesting `(7 / 2).floor()` — which answers 3 — is the same
|
|
893
|
+
* class of wrong rewrite the divisor side used to hand back. The sign is unary
|
|
894
|
+
* exactly when nothing that could end an operand stands in front of it, which
|
|
895
|
+
* keeps the binary reading in `a - 7 // 2`, where the dividend is still `7`.
|
|
896
|
+
*
|
|
897
|
+
* `target` is the name the value lands in, and only a plain `=` produces one:
|
|
898
|
+
* a compound assignment reads its target as well as writing it, so calling it
|
|
899
|
+
* the receiver would be a second claim this advisory has not checked.
|
|
900
|
+
*/
|
|
901
|
+
dividendBeforeComment(lineStart, commentStart) {
|
|
902
|
+
let index = this.tokens.length - 1;
|
|
903
|
+
let depth = 0;
|
|
904
|
+
while (index >= 0 && this.tokens[index].span.end > lineStart) {
|
|
905
|
+
const kind = this.tokens[index].kind;
|
|
906
|
+
if (kind === "rightParen" || kind === "rightBracket" || kind === "rightBrace")
|
|
907
|
+
depth += 1;
|
|
908
|
+
else if (kind === "leftParen" || kind === "leftBracket" || kind === "leftBrace") {
|
|
909
|
+
if (depth === 0)
|
|
910
|
+
break;
|
|
911
|
+
depth -= 1;
|
|
912
|
+
}
|
|
913
|
+
else if (depth === 0 && !primaryTailKinds.has(kind))
|
|
914
|
+
break;
|
|
915
|
+
index -= 1;
|
|
916
|
+
}
|
|
917
|
+
if (depth !== 0)
|
|
918
|
+
return null;
|
|
919
|
+
const sign = this.tokens[index];
|
|
920
|
+
if (sign !== undefined && sign.span.end > lineStart && (sign.kind === "minus" || sign.kind === "plus")) {
|
|
921
|
+
const before = this.tokens[index - 1];
|
|
922
|
+
const operandStandsBefore = before !== undefined
|
|
923
|
+
&& before.span.end > lineStart
|
|
924
|
+
&& !lineBoundaryKinds.has(before.kind)
|
|
925
|
+
&& (floorDivisionDividendEndKinds.has(before.kind) || before.kind === "rightBrace");
|
|
926
|
+
if (!operandStandsBefore)
|
|
927
|
+
index -= 1;
|
|
928
|
+
}
|
|
929
|
+
const first = this.tokens[index + 1];
|
|
930
|
+
if (!first || first.span.end <= lineStart)
|
|
931
|
+
return null;
|
|
932
|
+
// A leading-dot continuation line holds only the tail of its dividend: the
|
|
933
|
+
// walk is bounded by this physical line, so on `const c = xs` / `.size // 2`
|
|
934
|
+
// it stops at the line's own first token, the `.`. Quoting from there gave
|
|
935
|
+
// `'.size'` and suggested `(.size / 2).floor()`, which does not parse. The
|
|
936
|
+
// head lives on a line this advisory does not read, so there is no rewrite
|
|
937
|
+
// to name and D89's admission bar withholds the advisory (D90).
|
|
938
|
+
if (first.kind === "dot" || first.kind === "optionalDot")
|
|
939
|
+
return null;
|
|
940
|
+
const value = this.text.slice(first.span.start, commentStart).trim();
|
|
941
|
+
if (value === "")
|
|
942
|
+
return null;
|
|
943
|
+
const boundary = this.tokens[index];
|
|
944
|
+
const name = this.tokens[index - 1];
|
|
945
|
+
const assigned = boundary !== undefined && boundary.span.end > lineStart && boundary.kind === "assign";
|
|
946
|
+
return { target: assigned && name?.kind === "identifier" ? name.value : null, value };
|
|
422
947
|
}
|
|
423
948
|
readBlockComment() {
|
|
424
949
|
const start = this.index;
|
|
@@ -489,11 +1014,27 @@ export class Lexer {
|
|
|
489
1014
|
}
|
|
490
1015
|
const value = this.text.slice(start, this.index);
|
|
491
1016
|
const previous = this.tokens.at(-1)?.kind;
|
|
492
|
-
//
|
|
493
|
-
//
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
1017
|
+
// D90 (compiler-front-9): a rule's ban is on the spelling as a binding, a
|
|
1018
|
+
// parameter and a type; some of them are ordinary member names and record
|
|
1019
|
+
// keys. `int` remains forbidden as a type, but velar/random owns the method
|
|
1020
|
+
// spelling Random.int(...); `with` remains forbidden as the infix record
|
|
1021
|
+
// update, but `Array.prototype.with` and every builder API spelled that way
|
|
1022
|
+
// must be callable, and `{with: 1}` must be writable. The exemption is a
|
|
1023
|
+
// property of the rule (`memberLegal`) rather than a name spelled here, so
|
|
1024
|
+
// `eval` — which the charter keeps unavailable through direct member
|
|
1025
|
+
// syntax — does not travel with them.
|
|
1026
|
+
const declared = forbiddenSourceIdentifiers.get(value);
|
|
1027
|
+
// D90 (coherence): `def close(this)` used to earn two mechanical fixes on
|
|
1028
|
+
// one span — this rule's `this` -> `self` rewrite and the analyzer's
|
|
1029
|
+
// delete-the-implicit-receiver rewrite — whose texts contradict each
|
|
1030
|
+
// other. Applying the first produces `def close(self)`, which is itself an
|
|
1031
|
+
// error, so a `velar fix` pass never reaches a clean source. The receiver
|
|
1032
|
+
// parameter is the analyzer's report to make: it knows the declaration has
|
|
1033
|
+
// an implicit receiver, and its fix deletes the parameter outright. The
|
|
1034
|
+
// recovery token is still emitted, so the parameter arrives as `self` and
|
|
1035
|
+
// lands on exactly that report.
|
|
1036
|
+
const receiverParameter = value === "this" && this.isReceiverParameterPosition(previous);
|
|
1037
|
+
const rule = declared?.memberLegal === true && this.isMemberNamePosition(previous) ? undefined : declared;
|
|
497
1038
|
const extensionGuidance = rule ? undefined : this.extensionForbiddenIdentifiers.get(value);
|
|
498
1039
|
if ((value === "Infinity" || value === "NaN") && previous !== "dot" && previous !== "optionalDot") {
|
|
499
1040
|
this.diagnostics.push(diagnostic("VEL1007", value === "Infinity"
|
|
@@ -506,7 +1047,8 @@ export class Lexer {
|
|
|
506
1047
|
if (rule.recovery) {
|
|
507
1048
|
// The rule carries its successor only when the guidance names exactly
|
|
508
1049
|
// one ('var' names 'let' or 'const', so it names none).
|
|
509
|
-
|
|
1050
|
+
if (!receiverParameter)
|
|
1051
|
+
this.diagnostics.push(recoveredDiagnostic("VEL1005", rule.guidance, span(start, this.index), rule.fix === null ? undefined : mechanicalFix(span(start, rule.fix === "" ? this.skipHorizontalWhitespace(this.index) : this.index), rule.fix, rule.fix === "" ? `Remove '${value}'` : `Use '${rule.fix}'`)));
|
|
510
1052
|
for (const item of rule.recovery) {
|
|
511
1053
|
this.tokens.push({ kind: item.kind, value: item.value, span: span(start, this.index) });
|
|
512
1054
|
}
|
|
@@ -523,6 +1065,86 @@ export class Lexer {
|
|
|
523
1065
|
const keyword = Object.hasOwn(keywordKinds, value) ? keywordKinds[value] : undefined;
|
|
524
1066
|
this.tokens.push({ kind: keyword ?? "identifier", value, span: span(start, this.index) });
|
|
525
1067
|
}
|
|
1068
|
+
/**
|
|
1069
|
+
* The three positions in which a name is a member name rather than a binding:
|
|
1070
|
+
* after a member step, as a record-literal key, and as the name of a member
|
|
1071
|
+
* declared in a class body. The first two are the reads — `q.with("cte")`,
|
|
1072
|
+
* `{with: 1}` — and the third is the declaration an extern module needs to
|
|
1073
|
+
* describe such an API at all.
|
|
1074
|
+
*
|
|
1075
|
+
* A class body is the only place the declaration is legal: `def with(...)`
|
|
1076
|
+
* outside one binds a name, and the generated module would say
|
|
1077
|
+
* `function with`, which is not JavaScript.
|
|
1078
|
+
*/
|
|
1079
|
+
isMemberNamePosition(previous) {
|
|
1080
|
+
if (previous === "dot" || previous === "optionalDot")
|
|
1081
|
+
return true;
|
|
1082
|
+
// A record key is followed by ':' — `{with: 1}` and `{a: 1, with: 2}`. The
|
|
1083
|
+
// preceding '{' or ',' is what separates a key from an argument in a call.
|
|
1084
|
+
if ((previous === "leftBrace" || previous === "comma")
|
|
1085
|
+
&& this.text[this.skipHorizontalWhitespace(this.index)] === ":")
|
|
1086
|
+
return true;
|
|
1087
|
+
const declaring = previous === "def"
|
|
1088
|
+
|| (previous === "identifier" && (this.tokens.at(-1)?.value === "get" || this.tokens.at(-1)?.value === "set"));
|
|
1089
|
+
return declaring && (this.classBodyStack.at(-1) ?? false);
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* D90 (coherence): the one position where `this` is the Python receiver
|
|
1093
|
+
* reflex rather than JavaScript's dynamic receiver — a parameter name in the
|
|
1094
|
+
* list of an instance method or a constructor inside a class body. The
|
|
1095
|
+
* analyzer owns that report, because only it knows the declaration carries
|
|
1096
|
+
* an implicit receiver, and its rewrite deletes the parameter instead of
|
|
1097
|
+
* renaming it to a spelling that is an error in the same position.
|
|
1098
|
+
*
|
|
1099
|
+
* The walk is paren-balanced so a default value cannot be mistaken for the
|
|
1100
|
+
* list that encloses it — `def m(a = f(this))` is an ordinary receiver read
|
|
1101
|
+
* — and `static def make(this)` is excluded because a static method has no
|
|
1102
|
+
* receiver to delete, so the rename is still the honest answer there.
|
|
1103
|
+
*/
|
|
1104
|
+
isReceiverParameterPosition(previous) {
|
|
1105
|
+
if (previous !== "leftParen" && previous !== "comma")
|
|
1106
|
+
return false;
|
|
1107
|
+
if (!(this.classBodyStack.at(-1) ?? false))
|
|
1108
|
+
return false;
|
|
1109
|
+
let depth = 0;
|
|
1110
|
+
let index = this.tokens.length - 1;
|
|
1111
|
+
for (let steps = 0; index >= 0 && steps < RECEIVER_PARAMETER_SCAN_LIMIT; steps += 1, index -= 1) {
|
|
1112
|
+
const kind = this.tokens[index].kind;
|
|
1113
|
+
if (kind === "rightParen")
|
|
1114
|
+
depth += 1;
|
|
1115
|
+
else if (kind === "leftParen") {
|
|
1116
|
+
if (depth === 0)
|
|
1117
|
+
break;
|
|
1118
|
+
depth -= 1;
|
|
1119
|
+
}
|
|
1120
|
+
else if (lineBoundaryKinds.has(kind) && depth === 0 && kind !== "newline")
|
|
1121
|
+
return false;
|
|
1122
|
+
}
|
|
1123
|
+
if (index < 0 || this.tokens[index]?.kind !== "leftParen")
|
|
1124
|
+
return false;
|
|
1125
|
+
let head = index - 1;
|
|
1126
|
+
// A generic method writes its parameters after the type parameter list, so
|
|
1127
|
+
// `def m<T>(this)` has to walk back over a balanced '<...>' to reach the
|
|
1128
|
+
// name that says which declaration this list belongs to.
|
|
1129
|
+
if (this.tokens[head]?.kind === "greater") {
|
|
1130
|
+
let angle = 0;
|
|
1131
|
+
for (let steps = 0; head >= 0 && steps < RECEIVER_PARAMETER_SCAN_LIMIT; steps += 1, head -= 1) {
|
|
1132
|
+
const kind = this.tokens[head].kind;
|
|
1133
|
+
if (kind === "greater")
|
|
1134
|
+
angle += 1;
|
|
1135
|
+
else if (kind === "less" && (angle -= 1) === 0) {
|
|
1136
|
+
head -= 1;
|
|
1137
|
+
break;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
const name = this.tokens[head];
|
|
1142
|
+
if (name?.kind !== "identifier")
|
|
1143
|
+
return false;
|
|
1144
|
+
if (name.value === "constructor")
|
|
1145
|
+
return true;
|
|
1146
|
+
return this.tokens[head - 1]?.kind === "def" && this.tokens[head - 2]?.kind !== "static";
|
|
1147
|
+
}
|
|
526
1148
|
readNumber() {
|
|
527
1149
|
const start = this.index;
|
|
528
1150
|
if (this.peek() === "0" && ["x", "X", "b", "B", "o", "O"].includes(this.peek(1))) {
|
|
@@ -567,7 +1189,7 @@ export class Lexer {
|
|
|
567
1189
|
this.advance();
|
|
568
1190
|
const suffix = this.text.slice(numberEnd, this.index);
|
|
569
1191
|
if (suffix && this.numericSuffixes.has(suffix)) {
|
|
570
|
-
this.
|
|
1192
|
+
this.pushNumber("unitNumber", `${value}${suffix}`, span(start, this.index));
|
|
571
1193
|
return;
|
|
572
1194
|
}
|
|
573
1195
|
if (suffix) {
|
|
@@ -586,7 +1208,7 @@ export class Lexer {
|
|
|
586
1208
|
? `Unknown numeric unit '${suffix}'`
|
|
587
1209
|
: `Unexpected characters '${suffix}' after a number`, span(numberEnd, this.index)));
|
|
588
1210
|
}
|
|
589
|
-
this.
|
|
1211
|
+
this.pushNumber("number", value, span(start, numberEnd));
|
|
590
1212
|
}
|
|
591
1213
|
readRadixNumber() {
|
|
592
1214
|
const start = this.index;
|
|
@@ -623,7 +1245,24 @@ export class Lexer {
|
|
|
623
1245
|
this.diagnostics.push(diagnostic("VEL1007", `${radixName[0].toUpperCase()}${radixName.slice(1)} integer literals require at least one digit`, span(start, this.index)));
|
|
624
1246
|
digits = "0";
|
|
625
1247
|
}
|
|
626
|
-
this.
|
|
1248
|
+
this.pushNumber("number", `0${prefix}${digits}`, span(start, this.index));
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* A number token, carrying the author's own spelling whenever the value it
|
|
1252
|
+
* holds is spelled differently — `1_000` loses its separators and `0X20`
|
|
1253
|
+
* loses its uppercase prefix on the way to the token. D90 R6 quotes the
|
|
1254
|
+
* literal back when it is not exactly representable, and it must quote what
|
|
1255
|
+
* was written: reporting `'10000000000000000001'` for a source line that
|
|
1256
|
+
* reads `1_000_000_000_000_000_000_1` sends the author looking for text that
|
|
1257
|
+
* is not there.
|
|
1258
|
+
*/
|
|
1259
|
+
pushNumber(kind, value, tokenSpan) {
|
|
1260
|
+
const written = this.text.slice(tokenSpan.start, tokenSpan.end);
|
|
1261
|
+
if (written === value) {
|
|
1262
|
+
this.tokens.push({ kind, value, span: tokenSpan });
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
this.tokens.push({ kind, value, span: tokenSpan, payload: { written } });
|
|
627
1266
|
}
|
|
628
1267
|
isRadixDigit(character, radix) {
|
|
629
1268
|
if (character >= "0" && character <= "9")
|
|
@@ -656,7 +1295,7 @@ export class Lexer {
|
|
|
656
1295
|
value += this.readDigitsWithSeparators();
|
|
657
1296
|
}
|
|
658
1297
|
this.diagnostics.push(recoveredDiagnostic("VEL1007", `Write '${value}'; decimal literals require a digit before the point`, span(start, this.index), mechanicalFix(span(start, this.index), value, `Write '${value}'`)));
|
|
659
|
-
this.
|
|
1298
|
+
this.pushNumber("number", value, span(start, this.index));
|
|
660
1299
|
}
|
|
661
1300
|
readDigitsWithSeparators() {
|
|
662
1301
|
let value = "";
|
|
@@ -695,6 +1334,7 @@ export class Lexer {
|
|
|
695
1334
|
if (scanned.quote === "'") {
|
|
696
1335
|
this.diagnostics.push(diagnostic("VEL1005", "Use double quotes or backticks for strings; single-quoted strings are not part of VelarScript", span(start + scanned.prefixLength, Math.min(this.index, start + scanned.prefixLength + 1))));
|
|
697
1336
|
}
|
|
1337
|
+
this.adviseTemplateInterpolation(scanned, start);
|
|
698
1338
|
const payload = {
|
|
699
1339
|
prefixLength: scanned.prefixLength,
|
|
700
1340
|
quote: scanned.quote,
|
|
@@ -739,6 +1379,87 @@ export class Lexer {
|
|
|
739
1379
|
},
|
|
740
1380
|
});
|
|
741
1381
|
}
|
|
1382
|
+
/**
|
|
1383
|
+
* D89 A5/A6: JavaScript's `${...}` never interpolates in VelarScript. The
|
|
1384
|
+
* charter keeps it literal on purpose — generating JavaScript source is a
|
|
1385
|
+
* real use of these literals — so the spelling cannot become an error, and a
|
|
1386
|
+
* string that carries it compiles in silence with a meaning the JavaScript
|
|
1387
|
+
* reflex behind it did not intend. That is D89's admission shape exactly,
|
|
1388
|
+
* on both sides of the `f` prefix:
|
|
1389
|
+
*
|
|
1390
|
+
* - A5, a plain double-quoted or backtick string: nothing interpolates, the
|
|
1391
|
+
* `${name}` stays text. The way out is the `f` prefix with `{name}`.
|
|
1392
|
+
* - A6, an `f` or `rf` string: the `$` ahead of `{` keeps that brace
|
|
1393
|
+
* literal, so the author who wrote the prefix *and* the JavaScript
|
|
1394
|
+
* spelling still gets text. The way out is dropping the `$`.
|
|
1395
|
+
*
|
|
1396
|
+
* The non-triggers, each deliberate: an empty `${}` or an unclosed `${`
|
|
1397
|
+
* carries no expression to interpolate; a raw `r"..."` string is the author
|
|
1398
|
+
* asking for literal text by name, so the deliberate-literal reading wins
|
|
1399
|
+
* there (`rf` still triggers A6 — its rawness is about backslashes, not
|
|
1400
|
+
* about interpolation); a single-quoted string is already VEL1005; an
|
|
1401
|
+
* unterminated string is already VEL1003; and an inline `extern js` /
|
|
1402
|
+
* `unsafe js` block never reaches this method at all — it is scanned by
|
|
1403
|
+
* `readEmbeddedJavaScript`, where `${...}` is documented literal
|
|
1404
|
+
* JavaScript. One advisory speaks per literal: the rewrite names the whole
|
|
1405
|
+
* string, so a second occurrence adds nothing the first did not say.
|
|
1406
|
+
*
|
|
1407
|
+
* The fix is registered only where D38 §48's no-judgment bar holds: every
|
|
1408
|
+
* `${...}` well-formed, every body a plain dotted name path (see
|
|
1409
|
+
* `interpolationPathBody`), no `$` immediately ahead of an occurrence's own
|
|
1410
|
+
* `$` — JavaScript's `$${x}` spells a literal `$` before an interpolation,
|
|
1411
|
+
* and deleting the occurrence's `$` leaves `${x}`, whose surviving `$`
|
|
1412
|
+
* holds the brace literal all over again, so the quoted rewrite would not
|
|
1413
|
+
* interpolate — and, for A5, no bare brace outside the occurrences, because
|
|
1414
|
+
* the `f` prefix would turn `{` into an interpolation opener and `{{` into
|
|
1415
|
+
* a single literal brace. Everything else keeps the message and loses the
|
|
1416
|
+
* one-click edit.
|
|
1417
|
+
*
|
|
1418
|
+
* A bracket fragment is exempt for A1's reason: its lexer holds the
|
|
1419
|
+
* fragment's text rather than the module's, so a span there would not land
|
|
1420
|
+
* on the physical line a `velar-allow` reads.
|
|
1421
|
+
*/
|
|
1422
|
+
adviseTemplateInterpolation(scanned, start) {
|
|
1423
|
+
if (this.bracketFragment || !scanned.closed || scanned.quote === "'")
|
|
1424
|
+
return;
|
|
1425
|
+
if (scanned.raw && !scanned.interpolated)
|
|
1426
|
+
return;
|
|
1427
|
+
const scan = templateInterpolationScan(scanned.content, scanned);
|
|
1428
|
+
const first = scan.occurrences[0];
|
|
1429
|
+
if (!first)
|
|
1430
|
+
return;
|
|
1431
|
+
const sourceOffset = (index) => scanned.contentOffsets?.[index] ?? scanned.contentStart + index;
|
|
1432
|
+
const reportSpan = span(sourceOffset(first.dollar), sourceOffset(first.close) + 1);
|
|
1433
|
+
const quotedFirst = scanned.content.slice(first.dollar, first.close + 1);
|
|
1434
|
+
const shown = quotedFirst.length <= 40 ? quotedFirst : `${quotedFirst.slice(0, 39)}…`;
|
|
1435
|
+
const fixable = scan.allWellFormed
|
|
1436
|
+
&& (scanned.interpolated || !scan.bareBrace)
|
|
1437
|
+
&& scan.occurrences.every((occurrence) => interpolationPathBody.test(occurrence.body.trim())
|
|
1438
|
+
&& scanned.content[occurrence.dollar - 1] !== "$");
|
|
1439
|
+
const deletions = scan.occurrences.map((occurrence) => ({ span: span(sourceOffset(occurrence.dollar), sourceOffset(occurrence.dollar) + 1), text: "" }));
|
|
1440
|
+
if (scanned.interpolated) {
|
|
1441
|
+
const rewritten = fixable ? this.literalWithoutDollars(start, deletions) : null;
|
|
1442
|
+
this.advisories.push(advisory("A6", rewritten !== null && rewritten.length <= 60
|
|
1443
|
+
? `VelarScript interpolation is '{...}', and '$' keeps the brace after it literal even under the 'f' prefix, so this stays the characters '${shown}'; drop the '$' and write '${rewritten}'`
|
|
1444
|
+
: `VelarScript interpolation is '{...}', and '$' keeps the brace after it literal even under the 'f' prefix, so this stays the characters '${shown}'; drop the '$' and write '{${first.body.trim()}}'`, reportSpan, fixable ? mechanicalEdits(deletions, "Drop the '$' and interpolate") : undefined));
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
const edits = [{ span: span(start, start), text: "f" }, ...deletions];
|
|
1448
|
+
const rewritten = fixable ? `f${this.literalWithoutDollars(start, deletions)}` : null;
|
|
1449
|
+
this.advisories.push(advisory("A5", rewritten !== null && rewritten.length <= 60
|
|
1450
|
+
? `'\${...}' is literal text in a VelarScript string — only the 'f' prefix interpolates — so this stays the characters '${shown}'; write '${rewritten}'`
|
|
1451
|
+
: `'\${...}' is literal text in a VelarScript string — only the 'f' prefix interpolates — so this stays the characters '${shown}'; write '{${first.body.trim()}}' under an 'f' prefix`, reportSpan, fixable ? mechanicalEdits(edits, "Interpolate with an 'f' string") : undefined));
|
|
1452
|
+
}
|
|
1453
|
+
/** The literal's source text with each occurrence's `$` removed, for the message that quotes the rewrite. */
|
|
1454
|
+
literalWithoutDollars(start, deletions) {
|
|
1455
|
+
let text = "";
|
|
1456
|
+
let cursor = start;
|
|
1457
|
+
for (const deletion of deletions) {
|
|
1458
|
+
text += this.text.slice(cursor, deletion.span.start);
|
|
1459
|
+
cursor = deletion.span.end;
|
|
1460
|
+
}
|
|
1461
|
+
return text + this.text.slice(cursor, this.index);
|
|
1462
|
+
}
|
|
742
1463
|
diagnoseStringContents(scanned) {
|
|
743
1464
|
const sourceOffset = (index) => scanned.contentOffsets?.[index] ?? scanned.contentStart + index;
|
|
744
1465
|
for (let index = 0; index < scanned.content.length; index += 1) {
|
|
@@ -850,6 +1571,7 @@ export class Lexer {
|
|
|
850
1571
|
}
|
|
851
1572
|
this.tokens.push(result.token);
|
|
852
1573
|
this.diagnostics.push(...result.diagnostics ?? []);
|
|
1574
|
+
this.advisories.push(...result.advisories ?? []);
|
|
853
1575
|
this.index = result.nextOffset;
|
|
854
1576
|
this.atLineStart = result.startsLine ?? false;
|
|
855
1577
|
return true;
|
|
@@ -885,8 +1607,14 @@ export class Lexer {
|
|
|
885
1607
|
*/
|
|
886
1608
|
trailingSemicolonFix(start) {
|
|
887
1609
|
let end = start + 1;
|
|
888
|
-
|
|
889
|
-
end
|
|
1610
|
+
if (end < this.semicolonRunEnd) {
|
|
1611
|
+
end = this.semicolonRunEnd;
|
|
1612
|
+
}
|
|
1613
|
+
else {
|
|
1614
|
+
while (this.text[end] === ";" || this.text[end] === " " || this.text[end] === "\t")
|
|
1615
|
+
end += 1;
|
|
1616
|
+
this.semicolonRunEnd = end;
|
|
1617
|
+
}
|
|
890
1618
|
const rest = this.text.slice(end, this.lineEnd(end));
|
|
891
1619
|
if (rest.length > 0 && !rest.startsWith("//") && !rest.startsWith("/*"))
|
|
892
1620
|
return undefined;
|
|
@@ -969,15 +1697,49 @@ export class Lexer {
|
|
|
969
1697
|
return (codePoint >= 0 && codePoint <= 0x1f && codePoint !== 0x0a && codePoint !== 0x0d)
|
|
970
1698
|
|| (codePoint >= 0x7f && codePoint <= 0x9f);
|
|
971
1699
|
}
|
|
1700
|
+
/**
|
|
1701
|
+
* The start of the physical line `index` sits on. D90 (compiler-front-2): the
|
|
1702
|
+
* backward scan this used to be is O(column) per call, and its callers — A1,
|
|
1703
|
+
* the block-comment reader, the semicolon fix, and every opening bracket —
|
|
1704
|
+
* run once per token, so one long physical line cost O(n²). A line of 20000
|
|
1705
|
+
* semicolons took 673 ms and a 4 MiB one would have taken hours, with nothing
|
|
1706
|
+
* to stop it: a ';' produces no token, so `MAX_TOKENS` never fires.
|
|
1707
|
+
*
|
|
1708
|
+
* The offsets those callers ask about only move forward, so the scan is
|
|
1709
|
+
* carried across the file once and each call pays for the characters since
|
|
1710
|
+
* the last one. An earlier offset still falls back to the backward scan,
|
|
1711
|
+
* which is correct and, being off the hot path, is not the cost.
|
|
1712
|
+
*/
|
|
972
1713
|
lineStart(index) {
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1714
|
+
if (index < this.scannedTo) {
|
|
1715
|
+
let cursor = index;
|
|
1716
|
+
while (cursor > 0 && this.text[cursor - 1] !== "\n" && this.text[cursor - 1] !== "\r")
|
|
1717
|
+
cursor -= 1;
|
|
1718
|
+
return cursor;
|
|
1719
|
+
}
|
|
1720
|
+
while (this.scannedTo < index) {
|
|
1721
|
+
const character = this.text[this.scannedTo];
|
|
1722
|
+
this.scannedTo += 1;
|
|
1723
|
+
if (character === "\n" || character === "\r")
|
|
1724
|
+
this.scannedLineStart = this.scannedTo;
|
|
1725
|
+
}
|
|
1726
|
+
return this.scannedLineStart;
|
|
976
1727
|
}
|
|
1728
|
+
/**
|
|
1729
|
+
* The end of the physical line `index` sits on. Every offset between a line's
|
|
1730
|
+
* start and its end shares that end, so one line answers every call about it
|
|
1731
|
+
* once — which is what keeps a line of N semicolons from paying N forward
|
|
1732
|
+
* scans of its own tail.
|
|
1733
|
+
*/
|
|
977
1734
|
lineEnd(index) {
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1735
|
+
if (index >= this.cachedLineEndFrom && index <= this.cachedLineEnd)
|
|
1736
|
+
return this.cachedLineEnd;
|
|
1737
|
+
let cursor = index;
|
|
1738
|
+
while (cursor < this.text.length && this.text[cursor] !== "\n" && this.text[cursor] !== "\r")
|
|
1739
|
+
cursor += 1;
|
|
1740
|
+
this.cachedLineEndFrom = index;
|
|
1741
|
+
this.cachedLineEnd = cursor;
|
|
1742
|
+
return cursor;
|
|
981
1743
|
}
|
|
982
1744
|
invalidCharacter(character, start) {
|
|
983
1745
|
this.advance();
|