@descent-vtt/spec-guard 0.26.0 → 0.28.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/CHANGELOG.md +164 -0
- package/README.md +76 -28
- package/dist/cites.d.ts.map +1 -1
- package/dist/cites.js +11 -1
- package/dist/cites.js.map +1 -1
- package/dist/cli.d.ts +15 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +34 -9
- package/dist/cli.js.map +1 -1
- package/dist/engine.d.ts +48 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +224 -13
- package/dist/engine.js.map +1 -1
- package/dist/glob.d.ts +9 -0
- package/dist/glob.d.ts.map +1 -1
- package/dist/glob.js +23 -0
- package/dist/glob.js.map +1 -1
- package/dist/impact.d.ts +13 -9
- package/dist/impact.d.ts.map +1 -1
- package/dist/impact.js +5 -3
- package/dist/impact.js.map +1 -1
- package/dist/imports.d.ts +1 -1
- package/dist/imports.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/polyglot.d.ts +18 -37
- package/dist/polyglot.d.ts.map +1 -1
- package/dist/polyglot.js +759 -187
- package/dist/polyglot.js.map +1 -1
- package/dist/prove.d.ts.map +1 -1
- package/dist/prove.js +42 -14
- package/dist/prove.js.map +1 -1
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +13 -0
- package/dist/reporter.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +17 -13
- package/dist/runner.js.map +1 -1
- package/dist/scope.d.ts +22 -2
- package/dist/scope.d.ts.map +1 -1
- package/dist/scope.js +10 -2
- package/dist/scope.js.map +1 -1
- package/dist/structure.d.ts +3 -1
- package/dist/structure.d.ts.map +1 -1
- package/dist/structure.js +15 -4
- package/dist/structure.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/polyglot.js
CHANGED
|
@@ -81,7 +81,7 @@ function mask(source, language) {
|
|
|
81
81
|
// The opening delimiter is left visible so a reader can see that a literal
|
|
82
82
|
// starts here; everything inside it is blanked so no keyword can hide there.
|
|
83
83
|
const blanks = [...comments, ...strings.map(([start, end]) => [start + 1, end])];
|
|
84
|
-
return { code: maskRanges(source, blanks), strings, source, starts: lineStarts(source), unterminated };
|
|
84
|
+
return { code: maskRanges(source, blanks), strings, comments, source, starts: lineStarts(source), unterminated };
|
|
85
85
|
}
|
|
86
86
|
/** The literal that starts exactly at `offset`, or null. */
|
|
87
87
|
function literalAt(masked, offset) {
|
|
@@ -132,13 +132,25 @@ export function literalValue(raw) {
|
|
|
132
132
|
return text;
|
|
133
133
|
}
|
|
134
134
|
/* ------------------------------------------------------------------ reading */
|
|
135
|
-
const IDENTIFIER = /[A-Za-z_][A-Za-z0-9_]*/y;
|
|
136
135
|
/**
|
|
137
|
-
*
|
|
136
|
+
* A Go identifier: a letter, then letters and decimal digits, where a letter
|
|
137
|
+
* is `_` or a character of a Letter category, Lu, Ll, Lt, Lm or Lo, and a
|
|
138
|
+
* digit one of Nd (The Go Programming Language Specification, "Characters",
|
|
139
|
+
* "Letters and digits" and "Identifiers", go1.27, read on 2026-10-19). The
|
|
140
|
+
* page has those categories as Unicode 8.0 gives them; it follows from it,
|
|
141
|
+
* and is not on it, that a later Unicode's letters, which the Node that runs
|
|
142
|
+
* spec-guard has, are read as letters. A mark is no letter, so `cafe` with a
|
|
143
|
+
* combining accent is no identifier, and the text is not normalised: `café`
|
|
144
|
+
* written so is two characters of which the second is none.
|
|
145
|
+
*/
|
|
146
|
+
const IDENTIFIER = /[\p{L}_][\p{L}\p{Nd}_]*/uy;
|
|
147
|
+
/**
|
|
148
|
+
* Whitespace-skipping cursor over masked code, which a Go import and a Python
|
|
149
|
+
* dynamic import are read with.
|
|
138
150
|
*
|
|
139
151
|
* Exported so its edges can be pinned directly. Reaching "what does peek() do
|
|
140
|
-
* at the end of the file" through a
|
|
141
|
-
* nothing about
|
|
152
|
+
* at the end of the file" through a Go source string is possible but says
|
|
153
|
+
* nothing about Go.
|
|
142
154
|
*/
|
|
143
155
|
export class Reader {
|
|
144
156
|
text;
|
|
@@ -160,26 +172,6 @@ export class Reader {
|
|
|
160
172
|
peek() {
|
|
161
173
|
return this.text[this.index] ?? '';
|
|
162
174
|
}
|
|
163
|
-
eat(token) {
|
|
164
|
-
if (!this.text.startsWith(token, this.index))
|
|
165
|
-
return false;
|
|
166
|
-
this.index += token.length;
|
|
167
|
-
return true;
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* Eats a word only when it really is the whole word.
|
|
171
|
-
*
|
|
172
|
-
* `eat('static')` also matches the first six characters of a namespace called
|
|
173
|
-
* `staticfiles`, and would then read the dependency as `files`.
|
|
174
|
-
*/
|
|
175
|
-
keyword(word) {
|
|
176
|
-
if (!this.text.startsWith(word, this.index))
|
|
177
|
-
return false;
|
|
178
|
-
if (/[A-Za-z0-9_]/.test(this.text[this.index + word.length] ?? ''))
|
|
179
|
-
return false;
|
|
180
|
-
this.index += word.length;
|
|
181
|
-
return true;
|
|
182
|
-
}
|
|
183
175
|
identifier() {
|
|
184
176
|
IDENTIFIER.lastIndex = this.index;
|
|
185
177
|
const match = IDENTIFIER.exec(this.text);
|
|
@@ -188,49 +180,6 @@ export class Reader {
|
|
|
188
180
|
this.index += match[0].length;
|
|
189
181
|
return match[0];
|
|
190
182
|
}
|
|
191
|
-
/**
|
|
192
|
-
* Reads `a.b.c`, leaving out a C# alias qualifier.
|
|
193
|
-
*
|
|
194
|
-
* `global::A.B` and `Legacy::A.B` both name the namespace `A.B`: what comes
|
|
195
|
-
* before `::` says where to look it up - the global namespace, or the
|
|
196
|
-
* assembly an extern alias names - and is not part of the name. Until 0.10.2
|
|
197
|
-
* this read `global.A.B`, and no pattern written for `A.B` matched it.
|
|
198
|
-
*/
|
|
199
|
-
dotted() {
|
|
200
|
-
let parts = [];
|
|
201
|
-
for (;;) {
|
|
202
|
-
const part = this.identifier();
|
|
203
|
-
if (!part)
|
|
204
|
-
break;
|
|
205
|
-
parts.push(part);
|
|
206
|
-
if (this.eat('::'))
|
|
207
|
-
parts = [];
|
|
208
|
-
else if (!this.eat('.'))
|
|
209
|
-
break;
|
|
210
|
-
}
|
|
211
|
-
return parts.join('.');
|
|
212
|
-
}
|
|
213
|
-
/**
|
|
214
|
-
* Skips a generic argument list, so an alias target's `<...>` does not end
|
|
215
|
-
* the read.
|
|
216
|
-
*
|
|
217
|
-
* No bracket counting. A `using` directive ends at the first `;` or newline
|
|
218
|
-
* whatever is nested inside its angle brackets, and the caller's only
|
|
219
|
-
* question is whether the next non-space character is that `;` - so `A<B>`
|
|
220
|
-
* and `A<B<C>>` are both passed over by the same rule. The depth counter
|
|
221
|
-
* that used to be here could not change an answer for any input C# accepts,
|
|
222
|
-
* which is another way of saying no test could hold it in place.
|
|
223
|
-
*/
|
|
224
|
-
skipGenerics() {
|
|
225
|
-
if (this.peek() !== '<')
|
|
226
|
-
return;
|
|
227
|
-
while (this.index < this.text.length) {
|
|
228
|
-
const char = this.text[this.index];
|
|
229
|
-
if (char === ';' || char === '\n')
|
|
230
|
-
return;
|
|
231
|
-
this.index += 1;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
183
|
}
|
|
235
184
|
/** Offset of the matching close, given the depth-tracking characters. */
|
|
236
185
|
export function matchingClose(text, open, opener, closer) {
|
|
@@ -442,12 +391,18 @@ function readPython(collector) {
|
|
|
442
391
|
while ((match = PYTHON_STATEMENT.exec(line.text)) !== null) {
|
|
443
392
|
const tokens = pythonTokens(line.text, PYTHON_STATEMENT.lastIndex);
|
|
444
393
|
const modules = tokens === null ? null : pythonImports(tokens, match[4] === 'from', match[3] !== undefined);
|
|
445
|
-
if (modules === null)
|
|
446
|
-
continue;
|
|
447
394
|
// Reported on the line its first word stands on, at that line's head,
|
|
448
395
|
// as an import at the head of a logical line always was.
|
|
449
396
|
const first = match.index + match[1].length + match[2].length;
|
|
450
397
|
const [, offset] = line.starts.findLast(([at]) => at <= first);
|
|
398
|
+
// A statement that starts as an import and that Python refuses names no
|
|
399
|
+
// module, and is said, as everything that could not be read is: a rule
|
|
400
|
+
// that passes over it under --strict fails (ADR-0008, amended
|
|
401
|
+
// 2026-10-18).
|
|
402
|
+
if (modules === null) {
|
|
403
|
+
note(collector, 'refused', offset, 'a statement that starts as an import is one Python refuses, and no module is read from it');
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
451
406
|
// Python reads a name in the form NFKC gives it.
|
|
452
407
|
for (const module of modules)
|
|
453
408
|
emit(collector, module.normalize('NFKC'), 'import', offset);
|
|
@@ -468,6 +423,11 @@ function readPython(collector) {
|
|
|
468
423
|
}
|
|
469
424
|
/* ----------------------------------------------------------------------- go */
|
|
470
425
|
const GO_IMPORT = /\bimport\b/g;
|
|
426
|
+
/** Go's keywords, which no identifier is, so no package name ("Keywords"). */
|
|
427
|
+
const GO_KEYWORDS = new Set([
|
|
428
|
+
...['break', 'case', 'chan', 'const', 'continue', 'default', 'defer', 'else', 'fallthrough', 'for', 'func', 'go', 'goto'],
|
|
429
|
+
...['if', 'import', 'interface', 'map', 'package', 'range', 'return', 'select', 'struct', 'switch', 'type', 'var'],
|
|
430
|
+
]);
|
|
471
431
|
function readGo(collector) {
|
|
472
432
|
const { code } = collector.masked;
|
|
473
433
|
GO_IMPORT.lastIndex = 0;
|
|
@@ -484,11 +444,29 @@ function readGo(collector) {
|
|
|
484
444
|
GO_IMPORT.lastIndex = close;
|
|
485
445
|
continue;
|
|
486
446
|
}
|
|
487
|
-
// A single import may carry
|
|
488
|
-
|
|
447
|
+
// A single import may carry a package name, `import fmt2 "fmt"`, `import
|
|
448
|
+
// 名 "fmt"` or `import _ "x"`, or a `.` ("Import declarations"). A name at
|
|
449
|
+
// the end of its line has a semicolon put after it, and a comment that
|
|
450
|
+
// holds a line break ends the line too ("Semicolons", "Comments"): the
|
|
451
|
+
// import ends there, with no path, which Go refuses.
|
|
452
|
+
if (reader.peek() === '.') {
|
|
489
453
|
reader.index += 1;
|
|
490
|
-
|
|
491
|
-
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
const name = reader.identifier();
|
|
457
|
+
// On from the keyword, so that `import import "x"` is read as no
|
|
458
|
+
// import, and not as a second.
|
|
459
|
+
if (GO_KEYWORDS.has(name)) {
|
|
460
|
+
GO_IMPORT.lastIndex = reader.index;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
// With no name the space after `import` was passed over already, so
|
|
464
|
+
// this finds a line break only after one.
|
|
465
|
+
const after = reader.index;
|
|
466
|
+
reader.skipSpace();
|
|
467
|
+
if (code.slice(after, reader.index).includes('\n'))
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
492
470
|
reader.skipSpace();
|
|
493
471
|
const literal = literalAt(collector.masked, reader.index);
|
|
494
472
|
if (literal !== null)
|
|
@@ -496,94 +474,679 @@ function readGo(collector) {
|
|
|
496
474
|
}
|
|
497
475
|
}
|
|
498
476
|
/* --------------------------------------------------------------------- rust */
|
|
499
|
-
|
|
477
|
+
/** Rust's white space, Unicode's Pattern_White_Space, which a masked comment is too. */
|
|
478
|
+
const RUST_SPACE = String.raw `\t\n\v\f\r \u{85}\u{200e}\u{200f}\u{2028}\u{2029}`;
|
|
479
|
+
/**
|
|
480
|
+
* Where a use declaration or an extern crate starts. The keyword is a whole
|
|
481
|
+
* word, so not the tail of `r#use`, a raw identifier.
|
|
482
|
+
*/
|
|
483
|
+
const RUST_USE = new RegExp(String.raw `(?<![\p{XID_Continue}#])(?:use|extern[${RUST_SPACE}]+crate)(?!\p{XID_Continue})`, 'gu');
|
|
500
484
|
/**
|
|
501
|
-
*
|
|
485
|
+
* One token of a use declaration, after any white space: a name, raw or not,
|
|
486
|
+
* `$crate` or another of a macro's names, or a mark. Nothing need separate two
|
|
487
|
+
* tokens that cannot be read as one.
|
|
488
|
+
*/
|
|
489
|
+
const RUST_TOKEN = new RegExp(String.raw `[${RUST_SPACE}]*(?:(r#)?([\p{XID_Start}_]\p{XID_Continue}*)|(\$[\p{XID_Start}_]\p{XID_Continue}*|::|[{},*;]))`, 'uy');
|
|
490
|
+
/** Nothing but white space, to the end of the text. */
|
|
491
|
+
const RUST_BLANK = new RegExp(String.raw `^[${RUST_SPACE}]*$`, 'u');
|
|
492
|
+
/** What `r#` cannot make a raw identifier of. */
|
|
493
|
+
const RUST_NOT_RAW = new Set(['_', 'crate', 'self', 'Self', 'super']);
|
|
494
|
+
/**
|
|
495
|
+
* What no segment of a path, alias or crate name is unless written raw:
|
|
496
|
+
* `_`, and Rust's keywords in every edition, strict and reserved (Keywords,
|
|
497
|
+
* as read on 2026-10-19), `Self` among them, which no use path holds
|
|
498
|
+
* (Use declarations, "use Paths"). So a declaration left open is never read
|
|
499
|
+
* on into the next, whose `use` or `extern` is one. `crate`, `self` and
|
|
500
|
+
* `super` are not here: they stand where `rustPathRefused` lets them.
|
|
501
|
+
* `async`, `await` and `dyn` are keywords from the 2018 edition on, `try`
|
|
502
|
+
* from 2018 and `gen` from 2024, and the edition is in `Cargo.toml`, which
|
|
503
|
+
* the reader does not open, so each is read as the name it is before them.
|
|
504
|
+
*/
|
|
505
|
+
const RUST_NOT_SEGMENT = new Set([
|
|
506
|
+
'_',
|
|
507
|
+
...['as', 'break', 'const', 'continue', 'else', 'enum', 'extern', 'false', 'fn', 'for', 'if', 'impl', 'in', 'let', 'loop'],
|
|
508
|
+
...['match', 'mod', 'move', 'mut', 'pub', 'ref', 'return', 'Self', 'static', 'struct', 'trait', 'true', 'type', 'unsafe'],
|
|
509
|
+
...['use', 'where', 'while', 'abstract', 'become', 'box', 'do', 'final', 'macro', 'override', 'priv', 'typeof', 'unsized'],
|
|
510
|
+
...['virtual', 'yield'],
|
|
511
|
+
]);
|
|
512
|
+
/** The words a path starts with to name a module, which an import binds only under a name of its own. */
|
|
513
|
+
const RUST_PATH_KEYWORDS = new Set(['crate', 'self', 'super', '$crate']);
|
|
514
|
+
/** The token at `index`, or null where none starts. */
|
|
515
|
+
function rustToken(text, index) {
|
|
516
|
+
RUST_TOKEN.lastIndex = index;
|
|
517
|
+
const match = RUST_TOKEN.exec(text);
|
|
518
|
+
if (match === null)
|
|
519
|
+
return null;
|
|
520
|
+
const [, raw, word, mark] = match;
|
|
521
|
+
// `r#` stands only before a name.
|
|
522
|
+
if (raw !== undefined && RUST_NOT_RAW.has(word))
|
|
523
|
+
return null;
|
|
524
|
+
return { text: word?.normalize('NFC') ?? mark, name: word !== undefined, raw: raw !== undefined, end: RUST_TOKEN.lastIndex };
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Whether rustc refuses a path of a use tree for where `crate`, `self`,
|
|
528
|
+
* `super` or `$crate` stands in it (Paths, "Path qualifiers"), or for
|
|
529
|
+
* importing a module that it binds only under a name given with `as` (Use
|
|
530
|
+
* declarations, "Restrictions"), each as read on 2026-10-19, and each held
|
|
531
|
+
* to rustc 1.97.1 in every edition.
|
|
532
|
+
*/
|
|
533
|
+
function rustPathRefused({ segments, global, bare }) {
|
|
534
|
+
const last = segments.length - 1;
|
|
535
|
+
const misplaced = segments.some((segment, at) => {
|
|
536
|
+
// First, and not after a `::`.
|
|
537
|
+
if (segment === 'crate' || segment === '$crate')
|
|
538
|
+
return at > 0 || global;
|
|
539
|
+
// First and not after a `::`, or last after one: `a::self` names `a`.
|
|
540
|
+
// `::{self as root}` imports the crate root in the 2015 edition and is
|
|
541
|
+
// refused from 2018, which the reader cannot tell apart, so it is read.
|
|
542
|
+
if (segment === 'self')
|
|
543
|
+
return at < last && (at > 0 || global);
|
|
544
|
+
// Only after other `super`s, or after a first `self`: a `self` anywhere
|
|
545
|
+
// else before it is refused where it stands.
|
|
546
|
+
if (segment === 'super')
|
|
547
|
+
return global || !segments.slice(0, at).every((before) => before === 'super' || before === 'self');
|
|
548
|
+
return false;
|
|
549
|
+
});
|
|
550
|
+
// `use crate;`, `use self;`, `use super::super;` and `use a::{self}` with
|
|
551
|
+
// `a` one of them: the module is bound only under a name given with `as`.
|
|
552
|
+
// `use crate::{};` binds nothing, and rustc takes it. A trailing `self`
|
|
553
|
+
// is one of those words too, so `use crate::self;` is refused alike.
|
|
554
|
+
return misplaced || (bare && segments.every((segment) => RUST_PATH_KEYWORDS.has(segment)));
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* The paths one use tree names, segment by segment, read as the Rust
|
|
558
|
+
* Reference's grammar has it:
|
|
559
|
+
*
|
|
560
|
+
* (path ::)? * (path ::)? { tree, ... } path (as name)?
|
|
502
561
|
*
|
|
503
|
-
* `
|
|
504
|
-
*
|
|
505
|
-
*
|
|
562
|
+
* `refused` where rustc would refuse the text, and `overflow` past
|
|
563
|
+
* MAX_EXPANSION paths or groups nested that deep. `end` is where reading
|
|
564
|
+
* stopped, past the tree or at the token it could not read.
|
|
506
565
|
*/
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
// alias is read, and white space left at its end is taken off on the next
|
|
513
|
-
// line: one space on either side of `as` splits at a place that reads the
|
|
514
|
-
// same, so the pattern without either `+` is an equivalent mutant.
|
|
515
|
-
const leaf = text.split(/\s+as\s+/)[0];
|
|
516
|
-
const trimmed = leaf.replace(/[;\s]+$/, '').replace(/^::/, '');
|
|
517
|
-
return trimmed ? [trimmed] : [];
|
|
518
|
-
}
|
|
519
|
-
const close = matchingClose(text, open, '{', '}');
|
|
520
|
-
const prefix = text.slice(0, open).replace(/::\s*$/, '').trim();
|
|
521
|
-
const inner = text.slice(open + 1, close);
|
|
522
|
-
const parts = [];
|
|
523
|
-
let depth = 0;
|
|
524
|
-
let current = '';
|
|
525
|
-
for (const char of inner) {
|
|
526
|
-
if (char === '{')
|
|
527
|
-
depth += 1;
|
|
528
|
-
if (char === '}')
|
|
529
|
-
depth -= 1;
|
|
530
|
-
if (char === ',' && depth === 0) {
|
|
531
|
-
parts.push(current);
|
|
532
|
-
current = '';
|
|
533
|
-
continue;
|
|
534
|
-
}
|
|
535
|
-
current += char;
|
|
536
|
-
}
|
|
537
|
-
parts.push(current);
|
|
538
|
-
const results = [];
|
|
539
|
-
for (const part of parts) {
|
|
540
|
-
const expanded = expandUsePath(part);
|
|
541
|
-
if (expanded === null)
|
|
566
|
+
function readUseTree(text, from) {
|
|
567
|
+
let index = from;
|
|
568
|
+
const take = (accept) => {
|
|
569
|
+
const token = rustToken(text, index);
|
|
570
|
+
if (token === null || !accept(token))
|
|
542
571
|
return null;
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
572
|
+
index = token.end;
|
|
573
|
+
return token;
|
|
574
|
+
};
|
|
575
|
+
// No name is written as a mark is.
|
|
576
|
+
const mark = (text) => take((token) => token.text === text) !== null;
|
|
577
|
+
// A name, or `$crate` or another name of a macro's, which rustc reads
|
|
578
|
+
// where the macro is expanded.
|
|
579
|
+
const named = (token) => token.name || token.text.startsWith('$');
|
|
580
|
+
// `_` is a token of its own, and stands in a path only after `as`.
|
|
581
|
+
const segment = () => take((token) => named(token) && (token.raw || !RUST_NOT_SEGMENT.has(token.text)))?.text ?? null;
|
|
582
|
+
// A name given with `as`: `_`, or a name that is no keyword and no word a path starts with.
|
|
583
|
+
const alias = (token) => named(token) && (token.raw || token.text === '_' || !(RUST_NOT_SEGMENT.has(token.text) || RUST_PATH_KEYWORDS.has(token.text)));
|
|
584
|
+
const paths = [];
|
|
585
|
+
const tree = (head, rooted, depth) => {
|
|
586
|
+
if (depth > MAX_EXPANSION)
|
|
587
|
+
return 'overflow';
|
|
588
|
+
const path = [...head];
|
|
589
|
+
// A `::` before the first segment of all is the global root, and rustc
|
|
590
|
+
// refuses one anywhere else: `use a::{::b};`.
|
|
591
|
+
const leading = mark('::');
|
|
592
|
+
if (leading && head.length > 0)
|
|
593
|
+
return 'refused';
|
|
594
|
+
const global = rooted || leading;
|
|
595
|
+
for (let name = segment(); name !== null; name = segment()) {
|
|
596
|
+
path.push(name);
|
|
597
|
+
if (mark('::'))
|
|
598
|
+
continue;
|
|
599
|
+
const renamed = take((token) => !token.raw && token.text === 'as') !== null;
|
|
600
|
+
if (renamed && take(alias) === null)
|
|
601
|
+
return 'refused';
|
|
602
|
+
paths.push({ segments: path, global, bare: !renamed });
|
|
603
|
+
return 'read';
|
|
604
|
+
}
|
|
605
|
+
// A separator was the last token read, or nothing was.
|
|
606
|
+
if (mark('*')) {
|
|
607
|
+
// A glob binds no module of its own; its `*` is no word a path starts
|
|
608
|
+
// with either, so `bare` is never what decides it.
|
|
609
|
+
paths.push({ segments: [...path, '*'], global, bare: false });
|
|
610
|
+
return 'read';
|
|
548
611
|
}
|
|
612
|
+
if (!mark('{'))
|
|
613
|
+
return 'refused';
|
|
614
|
+
if (mark('}')) {
|
|
615
|
+
// An empty group imports nothing, and rustc still reads the path before
|
|
616
|
+
// it as an import: its lint names `use a::{};` an unused one.
|
|
617
|
+
paths.push({ segments: path, global, bare: false });
|
|
618
|
+
return 'read';
|
|
619
|
+
}
|
|
620
|
+
do {
|
|
621
|
+
const read = tree(path, global, depth + 1);
|
|
622
|
+
if (read !== 'read')
|
|
623
|
+
return read;
|
|
624
|
+
if (paths.length > MAX_EXPANSION)
|
|
625
|
+
return 'overflow';
|
|
626
|
+
} while (mark(',') && rustToken(text, index)?.text !== '}');
|
|
627
|
+
return mark('}') ? 'read' : 'refused';
|
|
628
|
+
};
|
|
629
|
+
const read = tree([], false, 0);
|
|
630
|
+
return { paths: read !== 'read' ? read : paths.some(rustPathRefused) ? 'refused' : paths, end: index };
|
|
631
|
+
}
|
|
632
|
+
/** The module a path of a use tree names, in the notation a rule writes, or nothing. */
|
|
633
|
+
function usePath({ segments }) {
|
|
634
|
+
// `a::b::self` names `a::b`, as `a::{self}` names `a`. A glob of nothing,
|
|
635
|
+
// the one path a `*` starts, names no module.
|
|
636
|
+
const named = segments.length > 1 && segments.at(-1) === 'self' ? segments.slice(0, -1) : segments;
|
|
637
|
+
return named[0] === '*' ? '' : named.join('::');
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Expands one use tree, with its `;` or without, into the module paths it
|
|
641
|
+
* names.
|
|
642
|
+
*
|
|
643
|
+
* `a::{b, c::{d, e}}` is three dependencies written as one declaration, and a
|
|
644
|
+
* rule about `a::c` has to see them. A tree is read as rustc reads it: white
|
|
645
|
+
* space and comments between its tokens, a raw identifier without its `r#`, a
|
|
646
|
+
* name in its NFC form, and `self` at the end of a path naming what is before
|
|
647
|
+
* it. Text that is no use tree, which rustc refuses, names nothing (ADR-0008,
|
|
648
|
+
* amended 2026-10-18), and so does a tree with a keyword for a segment, or
|
|
649
|
+
* with `crate`, `self` or `super` where rustc refuses it (amended
|
|
650
|
+
* 2026-10-19). A `$` name is read as a macro's, as a tree alone says nothing
|
|
651
|
+
* of where it stands. Returns null when the tree would name more than
|
|
652
|
+
* MAX_EXPANSION paths, or nest its groups deeper than that, which the caller
|
|
653
|
+
* reports rather than truncating.
|
|
654
|
+
*/
|
|
655
|
+
export function expandUsePath(input) {
|
|
656
|
+
const { paths, end } = readUseTree(input, 0);
|
|
657
|
+
if (paths === 'overflow')
|
|
658
|
+
return null;
|
|
659
|
+
const semicolon = rustToken(input, end);
|
|
660
|
+
const rest = input.slice(semicolon?.text === ';' ? semicolon.end : end);
|
|
661
|
+
return paths !== 'refused' && RUST_BLANK.test(rest) ? paths.map(usePath).filter((path) => path !== '') : [];
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* The crate an extern crate declaration names, read from after `crate`, or
|
|
665
|
+
* null where rustc refuses it: `extern crate name (as alias)? ;`, the name an
|
|
666
|
+
* identifier that is no keyword, or `self` with an alias, which rustc takes
|
|
667
|
+
* only so, and the end of its `;`.
|
|
668
|
+
*/
|
|
669
|
+
function externCrate(text, from) {
|
|
670
|
+
// A name that is no keyword and no word a path starts with, raw or not.
|
|
671
|
+
const name = (token) => token.name && (token.raw || !(RUST_NOT_SEGMENT.has(token.text) || RUST_PATH_KEYWORDS.has(token.text)));
|
|
672
|
+
const crate = rustToken(text, from);
|
|
673
|
+
if (crate === null || !(name(crate) || crate.text === 'self'))
|
|
674
|
+
return null;
|
|
675
|
+
let next = rustToken(text, crate.end);
|
|
676
|
+
const renamed = next !== null && !next.raw && next.text === 'as';
|
|
677
|
+
if (renamed) {
|
|
678
|
+
const alias = rustToken(text, next.end);
|
|
679
|
+
next = alias !== null && (name(alias) || alias.text === '_') ? rustToken(text, alias.end) : null;
|
|
549
680
|
}
|
|
550
|
-
return
|
|
681
|
+
return next?.text === ';' && (renamed || crate.text !== 'self') ? { crate: crate.text, end: next.end } : null;
|
|
551
682
|
}
|
|
683
|
+
/**
|
|
684
|
+
* Where a macro's tokens stand: the group of an invocation, `m!(...)`,
|
|
685
|
+
* `path::m![...]` or `m! {...}`, of a definition, `macro_rules! m {...}`
|
|
686
|
+
* and `macro m(...) {...}`, and of an attribute, `#[...]` and `#![...]`.
|
|
687
|
+
* rustc reads them as tokens and refuses none of them for not being a
|
|
688
|
+
* declaration: a matcher holds the word `use`, and `quote!` writes
|
|
689
|
+
* `use #name::Thing;`. Too wide a reach costs a note, never a reading: a
|
|
690
|
+
* declaration in one is read as everywhere else.
|
|
691
|
+
*/
|
|
692
|
+
const RUST_MACRO = new RegExp(String.raw `(?:(?<!\p{XID_Continue})macro_rules[${RUST_SPACE}]*![${RUST_SPACE}]*[\p{XID_Start}_]\p{XID_Continue}*|(?<![\p{XID_Continue}#])(macro)[${RUST_SPACE}]+[\p{XID_Start}_]\p{XID_Continue}*|\p{XID_Continue}!|#!?)[${RUST_SPACE}]*([(\[{])`,
|
|
693
|
+
// Emptied, the flags leave each `exec` in `macroRanges` reading from the
|
|
694
|
+
// start, and on a file with an attribute its loop pushes a range a turn
|
|
695
|
+
// for ever. The thread runs out of memory, and no test's timer fires
|
|
696
|
+
// inside the loop, so a sweep counts an error: the tests of a macro's
|
|
697
|
+
// tokens see the mutant where they run before any test that meets it.
|
|
698
|
+
'gu');
|
|
699
|
+
const RUST_CLOSERS = { '(': ')', '[': ']', '{': '}' };
|
|
700
|
+
/** The brace a `macro` definition's body opens after its parameters. */
|
|
701
|
+
const RUST_BODY = new RegExp(String.raw `[${RUST_SPACE}]*\{`, 'uy');
|
|
702
|
+
/** The half-open ranges of a file's macro tokens, outermost only, ascending. */
|
|
703
|
+
function macroRanges(code) {
|
|
704
|
+
const ranges = [];
|
|
705
|
+
RUST_MACRO.lastIndex = 0;
|
|
706
|
+
let match;
|
|
707
|
+
while ((match = RUST_MACRO.exec(code)) !== null) {
|
|
708
|
+
const opener = match[2];
|
|
709
|
+
let end = matchingClose(code, RUST_MACRO.lastIndex - 1, opener, RUST_CLOSERS[opener]) + 1;
|
|
710
|
+
// The body ends at its `}`, and `- 1` for its `+ 1` is an equivalent
|
|
711
|
+
// mutant: no `use` starts in the body's last two characters.
|
|
712
|
+
RUST_BODY.lastIndex = end;
|
|
713
|
+
if (match[1] !== undefined && RUST_BODY.test(code))
|
|
714
|
+
end = matchingClose(code, RUST_BODY.lastIndex - 1, '{', '}') + 1;
|
|
715
|
+
ranges.push([match.index, end]);
|
|
716
|
+
// Outermost only: each is read to its end once, so nesting costs nothing.
|
|
717
|
+
RUST_MACRO.lastIndex = end;
|
|
718
|
+
}
|
|
719
|
+
return ranges;
|
|
720
|
+
}
|
|
721
|
+
const RUST_SPACE_CHARACTER = new RegExp(`[${RUST_SPACE}]`, 'u');
|
|
722
|
+
/** Whether `at` is where an item or a statement may start: at the head of the file, or after `;`, a brace, an attribute's `]`, `pub` or its `(...)`. */
|
|
723
|
+
function startsAnItem(code, at) {
|
|
724
|
+
let before = at;
|
|
725
|
+
while (RUST_SPACE_CHARACTER.test(code[before - 1] ?? ''))
|
|
726
|
+
before -= 1;
|
|
727
|
+
return before === 0 || ';{}])'.includes(code[before - 1]) || /(?<!\p{XID_Continue})pub$/u.test(code.slice(Math.max(0, before - 4), before));
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* A doc comment, which rustc reads as an attribute: three slashes and no
|
|
731
|
+
* fourth, `//!`, a block opened with two stars and no third that holds
|
|
732
|
+
* something, and a block opened with `/*!` (Comments, as read on
|
|
733
|
+
* 2026-10-19).
|
|
734
|
+
*/
|
|
735
|
+
const RUST_DOC = /^(?:\/\/\/(?!\/)|\/\/!|\/\*\*(?![*/])|\/\*!)/;
|
|
552
736
|
function readRust(collector) {
|
|
553
|
-
const { code } = collector.masked;
|
|
737
|
+
const { code, comments, source } = collector.masked;
|
|
738
|
+
const macros = macroRanges(code);
|
|
739
|
+
let macro = 0;
|
|
740
|
+
let comment = 0;
|
|
554
741
|
RUST_USE.lastIndex = 0;
|
|
555
742
|
let match;
|
|
556
743
|
while ((match = RUST_USE.exec(code)) !== null) {
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
744
|
+
const at = match.index;
|
|
745
|
+
while (macro < macros.length && macros[macro][1] <= at)
|
|
746
|
+
macro += 1;
|
|
747
|
+
// A range starts at a macro's name or an attribute's `#`, never where a
|
|
748
|
+
// `use` does, so `<` for `<=` is an equivalent mutant; and no comment
|
|
749
|
+
// starts where a `use` does, so `<=` for the `<` after it is one too.
|
|
750
|
+
const inMacro = macro < macros.length && macros[macro][0] <= at;
|
|
751
|
+
while (comment < comments.length && comments[comment][0] < at)
|
|
752
|
+
comment += 1;
|
|
753
|
+
// A doc comment between `use` and the end of the declaration, which rustc
|
|
754
|
+
// reads as an attribute where no attribute may stand.
|
|
755
|
+
const documented = (end) => {
|
|
756
|
+
for (let next = comment; next < comments.length && comments[next][0] < end; next++) {
|
|
757
|
+
if (RUST_DOC.test(source.slice(comments[next][0], comments[next][0] + 4)))
|
|
758
|
+
return true;
|
|
759
|
+
}
|
|
760
|
+
return false;
|
|
761
|
+
};
|
|
762
|
+
// What rustc refuses is said where it starts as a declaration: where an
|
|
763
|
+
// item or a statement may start, and not among a macro's tokens, which
|
|
764
|
+
// rustc does not read as one (ADR-0008, amended 2026-10-19).
|
|
765
|
+
const refused = () => {
|
|
766
|
+
if (!inMacro && startsAnItem(code, at)) {
|
|
767
|
+
note(collector, 'refused', at, 'a declaration that starts as a use or an extern crate is one rustc refuses, and no module is read from it');
|
|
568
768
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
769
|
+
};
|
|
770
|
+
const start = at + match[0].length;
|
|
771
|
+
if (match[0] !== 'use') {
|
|
772
|
+
const extern = externCrate(code, start);
|
|
773
|
+
if (extern !== null && !documented(extern.end))
|
|
774
|
+
emit(collector, extern.crate, 'use', at);
|
|
775
|
+
else
|
|
776
|
+
refused();
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
const { paths, end } = readUseTree(code, start);
|
|
780
|
+
const semicolon = rustToken(code, end);
|
|
781
|
+
if (paths === 'overflow') {
|
|
782
|
+
note(collector, 'truncated', at, `a use declaration expands to more than ${MAX_EXPANSION} module paths, or nests its groups deeper than that`);
|
|
572
783
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
784
|
+
else if (paths !== 'refused' &&
|
|
785
|
+
semicolon?.text === ';' &&
|
|
786
|
+
!documented(semicolon.end) &&
|
|
787
|
+
// A `$` name stands only in a macro's tokens, where it is read as the
|
|
788
|
+
// macro is expanded.
|
|
789
|
+
(inMacro || !paths.some(({ segments }) => segments.some((segment) => segment.startsWith('$'))))) {
|
|
790
|
+
for (const path of paths)
|
|
791
|
+
emit(collector, usePath(path), 'use', at);
|
|
576
792
|
}
|
|
577
793
|
else {
|
|
578
|
-
|
|
579
|
-
emit(collector, specifier, 'use', match.index);
|
|
794
|
+
refused();
|
|
580
795
|
}
|
|
796
|
+
// On from where reading stopped, past the tree or at the token rustc
|
|
797
|
+
// would refuse, so that no token is read twice. No segment is `use` or
|
|
798
|
+
// `extern`, so no declaration is read into the next.
|
|
581
799
|
RUST_USE.lastIndex = end;
|
|
582
800
|
}
|
|
583
801
|
}
|
|
584
802
|
/* -------------------------------------------------------------------- c-sharp */
|
|
585
|
-
/**
|
|
586
|
-
const
|
|
803
|
+
/** What C# lets continue an identifier beside a letter: a mark, a digit, a connector, a formatting character. */
|
|
804
|
+
const CSHARP_PART = String.raw `\p{L}\p{Nl}\p{Mn}\p{Mc}\p{Nd}\p{Pc}\p{Cf}`;
|
|
805
|
+
/** C#'s white space, and what ends a line. */
|
|
806
|
+
const CSHARP_BLANK = String.raw `\p{Zs}\t\v\f`;
|
|
807
|
+
const CSHARP_BREAK = String.raw `\r\n\u{85}\u{2028}\u{2029}`;
|
|
808
|
+
/**
|
|
809
|
+
* Everything the C# reader stops at: a using, a namespace, or a brace. Each
|
|
810
|
+
* keyword is a whole word, so neither the identifier `@using` nor a word an
|
|
811
|
+
* escape goes on with.
|
|
812
|
+
*/
|
|
813
|
+
const CSHARP_TOKEN = new RegExp(String.raw `(?<![${CSHARP_PART}@\\])(?:using|namespace)(?![${CSHARP_PART}\\])|[{}]`, 'gu');
|
|
814
|
+
/**
|
|
815
|
+
* A pre-processing directive, which takes the whole of its line, so that the
|
|
816
|
+
* message of a `#region` is no code.
|
|
817
|
+
*/
|
|
818
|
+
const CSHARP_DIRECTIVE = new RegExp(String.raw `^[${CSHARP_BLANK}]*#[^${CSHARP_BREAK}]*`, 'gmu');
|
|
819
|
+
/**
|
|
820
|
+
* One token of a using directive or a namespace declaration, after any white
|
|
821
|
+
* space or end of a line, which a masked comment is too: a word, as an
|
|
822
|
+
* identifier and a keyword are both written, or a mark.
|
|
823
|
+
*/
|
|
824
|
+
const CSHARP_WORD = new RegExp(String.raw `[${CSHARP_BLANK}${CSHARP_BREAK}]*(?:(@?(?:[${CSHARP_PART}]|\\u[\dA-Fa-f]{4}|\\U[\dA-Fa-f]{8})+)|(::|[.;=<>,?*\[\](){}]))`, 'uy');
|
|
825
|
+
const CSHARP_ESCAPE = /\\u([\dA-Fa-f]{4})|\\U([\dA-Fa-f]{8})/g;
|
|
826
|
+
const CSHARP_IDENTIFIER = new RegExp(String.raw `^[\p{L}\p{Nl}_][${CSHARP_PART}]*$`, 'u');
|
|
827
|
+
/**
|
|
828
|
+
* C#'s keywords (6.4.4, as read on 2026-10-19), which no identifier is
|
|
829
|
+
* unless written with `@`, or with an escape in it, which no keyword holds.
|
|
830
|
+
* Its contextual keywords, `global`, `var` and `dynamic` among them, are
|
|
831
|
+
* identifiers where a name stands.
|
|
832
|
+
*/
|
|
833
|
+
const CSHARP_KEYWORDS = new Set([
|
|
834
|
+
...['abstract', 'as', 'base', 'bool', 'break', 'byte', 'case', 'catch', 'char', 'checked', 'class', 'const', 'continue'],
|
|
835
|
+
...['decimal', 'default', 'delegate', 'do', 'double', 'else', 'enum', 'event', 'explicit', 'extern', 'false', 'finally'],
|
|
836
|
+
...['fixed', 'float', 'for', 'foreach', 'goto', 'if', 'implicit', 'in', 'int', 'interface', 'internal', 'is', 'lock'],
|
|
837
|
+
...['long', 'namespace', 'new', 'null', 'object', 'operator', 'out', 'override', 'params', 'private', 'protected'],
|
|
838
|
+
...['public', 'readonly', 'ref', 'return', 'sbyte', 'sealed', 'short', 'sizeof', 'stackalloc', 'static', 'string'],
|
|
839
|
+
...['struct', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'uint', 'ulong', 'unchecked', 'unsafe', 'ushort'],
|
|
840
|
+
...['using', 'virtual', 'void', 'volatile', 'while'],
|
|
841
|
+
]);
|
|
842
|
+
/** The keywords that name a type (8.2.1, 8.3.1): `void` is one only before a `*`. */
|
|
843
|
+
const CSHARP_TYPE_KEYWORDS = new Set([
|
|
844
|
+
...['bool', 'byte', 'char', 'decimal', 'double', 'float', 'int', 'long', 'object', 'sbyte', 'short', 'string', 'uint'],
|
|
845
|
+
...['ulong', 'ushort'],
|
|
846
|
+
]);
|
|
847
|
+
/**
|
|
848
|
+
* The identifier a word is, as C# compares two: with no `@`, each escape the
|
|
849
|
+
* character it stands for, and no formatting character. Null for a word that
|
|
850
|
+
* is no identifier: a digit first, or an escape of what no identifier holds.
|
|
851
|
+
*/
|
|
852
|
+
function csharpIdentifier(word) {
|
|
853
|
+
// A word holds an `@` only as its first character.
|
|
854
|
+
const decoded = word.replace('@', '').replace(CSHARP_ESCAPE, (_, short, long) => {
|
|
855
|
+
try {
|
|
856
|
+
return String.fromCodePoint(Number.parseInt(short ?? long, 16));
|
|
857
|
+
}
|
|
858
|
+
catch {
|
|
859
|
+
// \U can write a number no character has. A dot is no part of a name.
|
|
860
|
+
return '.';
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
return CSHARP_IDENTIFIER.test(decoded) ? decoded.replace(/\p{Cf}/gu, '') : null;
|
|
864
|
+
}
|
|
865
|
+
/** A cursor over the tokens of one directive or declaration. */
|
|
866
|
+
class CsharpTokens {
|
|
867
|
+
text;
|
|
868
|
+
index;
|
|
869
|
+
/** How deep the type being read is. */
|
|
870
|
+
nesting = 0;
|
|
871
|
+
/** Whether a type nested deeper than MAX_EXPANSION, and was not read. */
|
|
872
|
+
overflow = false;
|
|
873
|
+
/** The name of each type read, in the order written: a type is its name, then its arguments. */
|
|
874
|
+
names = [];
|
|
875
|
+
constructor(text, index) {
|
|
876
|
+
this.text = text;
|
|
877
|
+
this.index = index;
|
|
878
|
+
}
|
|
879
|
+
next() {
|
|
880
|
+
CSHARP_WORD.lastIndex = this.index;
|
|
881
|
+
const match = CSHARP_WORD.exec(this.text);
|
|
882
|
+
return match === null ? null : { word: match[1], mark: match[2], end: CSHARP_WORD.lastIndex };
|
|
883
|
+
}
|
|
884
|
+
mark(mark) {
|
|
885
|
+
const token = this.next();
|
|
886
|
+
if (token?.mark !== mark)
|
|
887
|
+
return false;
|
|
888
|
+
this.index = token.end;
|
|
889
|
+
return true;
|
|
890
|
+
}
|
|
891
|
+
/** A keyword, as it is written: `@static`, and `static` with an escape in it, are identifiers. */
|
|
892
|
+
keyword(word) {
|
|
893
|
+
const token = this.next();
|
|
894
|
+
if (token?.word !== word)
|
|
895
|
+
return false;
|
|
896
|
+
this.index = token.end;
|
|
897
|
+
return true;
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* An identifier, which is never a keyword as it is written: so `using
|
|
901
|
+
* class.N;` names nothing, and a directive left open is never read on
|
|
902
|
+
* into the next, whose `using` or `namespace` is one.
|
|
903
|
+
*/
|
|
904
|
+
identifier() {
|
|
905
|
+
const token = this.next();
|
|
906
|
+
const name = token?.word === undefined || CSHARP_KEYWORDS.has(token.word) ? null : csharpIdentifier(token.word);
|
|
907
|
+
if (name !== null)
|
|
908
|
+
this.index = token.end;
|
|
909
|
+
return name;
|
|
910
|
+
}
|
|
911
|
+
/** `name (. name)*`, the name of a namespace declaration. */
|
|
912
|
+
qualified() {
|
|
913
|
+
const parts = [];
|
|
914
|
+
for (let part = this.identifier(); part !== null; part = this.identifier()) {
|
|
915
|
+
parts.push(part);
|
|
916
|
+
if (!this.mark('.'))
|
|
917
|
+
return parts.join('.');
|
|
918
|
+
}
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* A namespace or type name, `(alias ::)? name (. name)*`, or null. What
|
|
923
|
+
* stands before `::` says where to look the name up, the global namespace
|
|
924
|
+
* or an extern alias, and is no part of it. Where `generic` allows them, a
|
|
925
|
+
* type argument list after a name is passed over.
|
|
926
|
+
*/
|
|
927
|
+
name(generic) {
|
|
928
|
+
let part = this.identifier();
|
|
929
|
+
if (part !== null && this.mark('::'))
|
|
930
|
+
part = this.identifier();
|
|
931
|
+
const parts = [];
|
|
932
|
+
for (; part !== null; part = this.identifier()) {
|
|
933
|
+
parts.push(part);
|
|
934
|
+
if (generic && !this.typeArguments())
|
|
935
|
+
return null;
|
|
936
|
+
if (!this.mark('.'))
|
|
937
|
+
return parts.join('.');
|
|
938
|
+
}
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Passes over a type argument list, `<` to the `>` that closes it, each
|
|
943
|
+
* argument a type and none a pointer, which the compiler refuses there;
|
|
944
|
+
* false where something else stands. Token by token, so it never reads
|
|
945
|
+
* past a `;`, a brace or the next directive.
|
|
946
|
+
*/
|
|
947
|
+
typeArguments() {
|
|
948
|
+
if (!this.mark('<'))
|
|
949
|
+
return true;
|
|
950
|
+
do {
|
|
951
|
+
if (!this.type(false))
|
|
952
|
+
return false;
|
|
953
|
+
} while (this.mark(','));
|
|
954
|
+
return this.mark('>');
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Passes over a type, as the specification has one (8.1 and 24.3): a
|
|
958
|
+
* tuple of two or more, a keyword that names a type, a function pointer
|
|
959
|
+
* or a name with its type arguments, then the marks of a nullable, an
|
|
960
|
+
* array and a pointer. A pointer, a function pointer among them, only where
|
|
961
|
+
* `pointers` lets one stand: in an alias that is `unsafe`, and never in a
|
|
962
|
+
* tuple or a type argument. False where the text is none, and where it
|
|
963
|
+
* nests deeper than MAX_EXPANSION, as a Rust use group may not, with
|
|
964
|
+
* `overflow` set: so no type nested twenty thousand deep runs the reader
|
|
965
|
+
* out of stack.
|
|
966
|
+
*/
|
|
967
|
+
type(pointers) {
|
|
968
|
+
if (this.nesting >= MAX_EXPANSION) {
|
|
969
|
+
this.overflow = true;
|
|
970
|
+
return false;
|
|
971
|
+
}
|
|
972
|
+
this.nesting += 1;
|
|
973
|
+
const read = this.typeWithin(pointers);
|
|
974
|
+
this.nesting -= 1;
|
|
975
|
+
return read;
|
|
976
|
+
}
|
|
977
|
+
typeWithin(pointers) {
|
|
978
|
+
let pointer = false;
|
|
979
|
+
// Where no word is next, at the end of the text or before a mark, `''`
|
|
980
|
+
// is none of what a word is compared with, `delegate`, `void` and the
|
|
981
|
+
// type keywords: any other such string for it is an equivalent mutant.
|
|
982
|
+
const word = this.next()?.word ?? '';
|
|
983
|
+
if (this.mark('(')) {
|
|
984
|
+
let elements = 0;
|
|
985
|
+
do {
|
|
986
|
+
if (!this.type(false))
|
|
987
|
+
return false;
|
|
988
|
+
// The name an element may have.
|
|
989
|
+
this.identifier();
|
|
990
|
+
elements += 1;
|
|
991
|
+
} while (this.mark(','));
|
|
992
|
+
if (elements < 2 || !this.mark(')'))
|
|
993
|
+
return false;
|
|
994
|
+
}
|
|
995
|
+
else if (word === 'delegate' || word === 'void') {
|
|
996
|
+
this.keyword(word);
|
|
997
|
+
if (!this.mark('*') || (word === 'delegate' && !this.functionPointer()))
|
|
998
|
+
return false;
|
|
999
|
+
pointer = true;
|
|
1000
|
+
}
|
|
1001
|
+
else if (CSHARP_TYPE_KEYWORDS.has(word)) {
|
|
1002
|
+
this.keyword(word);
|
|
1003
|
+
}
|
|
1004
|
+
else {
|
|
1005
|
+
// Before the names its arguments hold, which reading it put here.
|
|
1006
|
+
const at = this.names.length;
|
|
1007
|
+
const name = this.name(true);
|
|
1008
|
+
if (name === null)
|
|
1009
|
+
return false;
|
|
1010
|
+
this.names.splice(at, 0, name);
|
|
1011
|
+
}
|
|
1012
|
+
for (;;) {
|
|
1013
|
+
if (this.mark('?'))
|
|
1014
|
+
continue;
|
|
1015
|
+
if (this.mark('*')) {
|
|
1016
|
+
pointer = true;
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
if (!this.mark('['))
|
|
1020
|
+
return pointers || !pointer;
|
|
1021
|
+
while (this.mark(','))
|
|
1022
|
+
continue;
|
|
1023
|
+
if (!this.mark(']'))
|
|
1024
|
+
return false;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* What follows `delegate*`: a calling convention or none, then `<`, the
|
|
1029
|
+
* parameters, each `ref`, `ref readonly`, `in` or `out` or none, and the
|
|
1030
|
+
* return type, `void` among them, then `>` (24.3.3).
|
|
1031
|
+
*/
|
|
1032
|
+
functionPointer() {
|
|
1033
|
+
if (this.keyword('unmanaged') && this.mark('[')) {
|
|
1034
|
+
do {
|
|
1035
|
+
if (this.identifier() === null)
|
|
1036
|
+
return false;
|
|
1037
|
+
} while (this.mark(','));
|
|
1038
|
+
if (!this.mark(']'))
|
|
1039
|
+
return false;
|
|
1040
|
+
}
|
|
1041
|
+
else {
|
|
1042
|
+
this.keyword('managed');
|
|
1043
|
+
}
|
|
1044
|
+
if (!this.mark('<'))
|
|
1045
|
+
return false;
|
|
1046
|
+
do {
|
|
1047
|
+
if (this.keyword('ref'))
|
|
1048
|
+
this.keyword('readonly');
|
|
1049
|
+
else if (!this.keyword('in'))
|
|
1050
|
+
this.keyword('out');
|
|
1051
|
+
if (!this.keyword('void') && !this.type(true))
|
|
1052
|
+
return false;
|
|
1053
|
+
} while (this.mark(','));
|
|
1054
|
+
return this.mark('>');
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* The namespaces and types a using directive names, read from after
|
|
1059
|
+
* `using`, or null where C# would refuse it:
|
|
1060
|
+
*
|
|
1061
|
+
* using static unsafe? type ; using unsafe? alias = name-or-type ; using namespace ;
|
|
1062
|
+
*
|
|
1063
|
+
* `global` before `using` changes nothing of it. A type is read whole: a
|
|
1064
|
+
* name, then each type its arguments name, and each element of a tuple; a
|
|
1065
|
+
* keyword that names a type, `int` or `string`, names nothing, and nor does
|
|
1066
|
+
* a tuple element's own name, and an alias of a type that names nothing is
|
|
1067
|
+
* no refusal. A using statement and a using declaration are none of these,
|
|
1068
|
+
* and are read as nothing.
|
|
1069
|
+
*/
|
|
1070
|
+
function usingDirective(tokens) {
|
|
1071
|
+
let target;
|
|
1072
|
+
if (tokens.keyword('static')) {
|
|
1073
|
+
tokens.keyword('unsafe');
|
|
1074
|
+
const name = tokens.name(true);
|
|
1075
|
+
target = name === null ? null : [name, ...tokens.names];
|
|
1076
|
+
}
|
|
1077
|
+
else {
|
|
1078
|
+
const unsafe = tokens.keyword('unsafe');
|
|
1079
|
+
const start = tokens.index;
|
|
1080
|
+
if (tokens.identifier() !== null && tokens.mark('=')) {
|
|
1081
|
+
// An alias may name any type (14.6.2), and a pointer of one where it
|
|
1082
|
+
// is `unsafe`.
|
|
1083
|
+
if (!tokens.type(unsafe))
|
|
1084
|
+
return null;
|
|
1085
|
+
target = tokens.names;
|
|
1086
|
+
}
|
|
1087
|
+
else if (unsafe) {
|
|
1088
|
+
return null;
|
|
1089
|
+
}
|
|
1090
|
+
else {
|
|
1091
|
+
tokens.index = start;
|
|
1092
|
+
const name = tokens.name(false);
|
|
1093
|
+
target = name === null ? null : [name];
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
// A type named twice, `G.L<G.L<int>>`, is one dependency. A refused
|
|
1097
|
+
// directive stays null, not an empty list, which is what says it refused.
|
|
1098
|
+
return tokens.mark(';') && target !== null ? [...new Set(target)] : null;
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Whether the tokens after a `using` start a using statement or a using
|
|
1102
|
+
* declaration, which top-level statements may hold where a directive may
|
|
1103
|
+
* stand: `(`, or a type and the name of a local, `using var x = ...;` and
|
|
1104
|
+
* `using N.D? d = null;` among them. Each is a statement the compiler reads
|
|
1105
|
+
* as one, refused or not, and no directive it refuses.
|
|
1106
|
+
*/
|
|
1107
|
+
function startsAStatement(code, from) {
|
|
1108
|
+
const tokens = new CsharpTokens(code, from);
|
|
1109
|
+
return tokens.mark('(') || (tokens.type(true) && tokens.identifier() !== null);
|
|
1110
|
+
}
|
|
1111
|
+
const CSHARP_SPACE_CHARACTER = new RegExp(`[${CSHARP_BLANK}${CSHARP_BREAK}]`, 'u');
|
|
1112
|
+
const CSHARP_GLOBAL_BEFORE = new RegExp(String.raw `(?<![${CSHARP_PART}@\\])global$`, 'u');
|
|
1113
|
+
/**
|
|
1114
|
+
* Whether a `using` at `at` stands where a directive may start: at the head
|
|
1115
|
+
* of the file, after `;` or a brace, or after `global`. Where it follows
|
|
1116
|
+
* anything else, `await` or an `)`, it starts a statement.
|
|
1117
|
+
*/
|
|
1118
|
+
function startsADirective(code, at) {
|
|
1119
|
+
let before = at;
|
|
1120
|
+
while (CSHARP_SPACE_CHARACTER.test(code[before - 1] ?? ''))
|
|
1121
|
+
before -= 1;
|
|
1122
|
+
return before === 0 || ';{}'.includes(code[before - 1]) || CSHARP_GLOBAL_BEFORE.test(code.slice(Math.max(0, before - 7), before));
|
|
1123
|
+
}
|
|
1124
|
+
/** The `#if` or `#endif` a pre-processing line is, or neither. */
|
|
1125
|
+
const CSHARP_CONDITION = new RegExp(String.raw `^[${CSHARP_BLANK}]*#[${CSHARP_BLANK}]*(if|endif)(?![${CSHARP_PART}])`, 'u');
|
|
1126
|
+
/**
|
|
1127
|
+
* The half-open ranges from each outermost `#if` to the end of its `#endif`.
|
|
1128
|
+
* The compiler skips the text of a section whose condition is false and reads
|
|
1129
|
+
* none of it (6.5.5), and which condition holds is the build's to say: so a
|
|
1130
|
+
* directive there is read as one anywhere else, and none is said to be
|
|
1131
|
+
* refused. One left open runs to the end of the file.
|
|
1132
|
+
*/
|
|
1133
|
+
function conditionalRanges(code) {
|
|
1134
|
+
const ranges = [];
|
|
1135
|
+
let depth = 0;
|
|
1136
|
+
let start = 0;
|
|
1137
|
+
CSHARP_DIRECTIVE.lastIndex = 0;
|
|
1138
|
+
let line;
|
|
1139
|
+
while ((line = CSHARP_DIRECTIVE.exec(code)) !== null) {
|
|
1140
|
+
const kind = CSHARP_CONDITION.exec(line[0])?.[1];
|
|
1141
|
+
if (kind === 'if' && depth++ === 0)
|
|
1142
|
+
start = line.index;
|
|
1143
|
+
if (kind === 'endif' && depth > 0 && --depth === 0)
|
|
1144
|
+
ranges.push([start, line.index + line[0].length]);
|
|
1145
|
+
}
|
|
1146
|
+
if (depth > 0)
|
|
1147
|
+
ranges.push([start, code.length]);
|
|
1148
|
+
return ranges;
|
|
1149
|
+
}
|
|
587
1150
|
/**
|
|
588
1151
|
* Reads using directives and the namespaces a file declares, in one pass.
|
|
589
1152
|
*
|
|
@@ -597,10 +1160,24 @@ const CSHARP_TOKEN = /\b(?:using|namespace)\b|[{}]/g;
|
|
|
597
1160
|
* the brace depth falls below its body's, so nothing here compares an offset
|
|
598
1161
|
* with the offset of a delimiter - which no keyword can ever sit on, and which
|
|
599
1162
|
* would leave `<` and `<=` meaning the same thing to every test. The code is
|
|
600
|
-
* masked, so no brace in a string or a
|
|
1163
|
+
* masked, so no brace in a string, a comment or a pre-processing directive
|
|
1164
|
+
* counts.
|
|
1165
|
+
*
|
|
1166
|
+
* A directive and a declaration are read token by token, as the C#
|
|
1167
|
+
* specification has them (ADR-0008, amended 2026-10-18): white space and
|
|
1168
|
+
* comments between the tokens, an identifier as C# compares two, and what
|
|
1169
|
+
* C# refuses read as nothing. Each type a directive names is read whole,
|
|
1170
|
+
* the types its arguments name and a tuple's elements among them, and each
|
|
1171
|
+
* is a reference. What it refuses is said where it starts as a directive:
|
|
1172
|
+
* in no brace but a namespace's, where a directive may start, where no
|
|
1173
|
+
* statement does, and outside every `#if` (amended 2026-10-19).
|
|
601
1174
|
*/
|
|
602
1175
|
function readCsharp(collector) {
|
|
603
|
-
|
|
1176
|
+
// A byte order mark at the head of a file is no character of its text.
|
|
1177
|
+
const marked = collector.masked.code.replace(/^\u{feff}/u, ' ');
|
|
1178
|
+
const conditions = conditionalRanges(marked);
|
|
1179
|
+
let condition = 0;
|
|
1180
|
+
const code = marked.replace(CSHARP_DIRECTIVE, (line) => ' '.repeat(line.length));
|
|
604
1181
|
const declared = new Set();
|
|
605
1182
|
const open = [];
|
|
606
1183
|
let fileScoped;
|
|
@@ -619,52 +1196,47 @@ function readCsharp(collector) {
|
|
|
619
1196
|
open.pop();
|
|
620
1197
|
continue;
|
|
621
1198
|
}
|
|
622
|
-
const
|
|
623
|
-
reader.index = match.index + token.length;
|
|
624
|
-
reader.skipSpace();
|
|
1199
|
+
const tokens = new CsharpTokens(code, CSHARP_TOKEN.lastIndex);
|
|
625
1200
|
if (token === 'namespace') {
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
declared.add(full);
|
|
643
|
-
continue;
|
|
1201
|
+
const name = tokens.qualified();
|
|
1202
|
+
if (name !== null) {
|
|
1203
|
+
const scoped = tokens.mark(';');
|
|
1204
|
+
if (scoped || tokens.mark('{')) {
|
|
1205
|
+
const outer = open.at(-1);
|
|
1206
|
+
const full = outer === undefined ? name : `${outer.name}.${name}`;
|
|
1207
|
+
// A file-scoped namespace covers the rest of the file, and C# allows
|
|
1208
|
+
// neither a second one nor a block beside it. A block's brace is
|
|
1209
|
+
// read here, and counted.
|
|
1210
|
+
if (scoped)
|
|
1211
|
+
fileScoped = full;
|
|
1212
|
+
else
|
|
1213
|
+
open.push({ name: full, depth: (depth += 1) });
|
|
1214
|
+
declared.add(full);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
644
1217
|
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
1218
|
+
else {
|
|
1219
|
+
const targets = usingDirective(tokens);
|
|
1220
|
+
// A section starts at the head of its `#if` line and ends at its
|
|
1221
|
+
// `#endif` line's end, where no `using` starts: `<` for either `<=`
|
|
1222
|
+
// is an equivalent mutant.
|
|
1223
|
+
while (condition < conditions.length && conditions[condition][1] <= match.index)
|
|
1224
|
+
condition += 1;
|
|
1225
|
+
const conditional = condition < conditions.length && conditions[condition][0] <= match.index;
|
|
1226
|
+
if (targets !== null) {
|
|
1227
|
+
for (const target of targets)
|
|
1228
|
+
emit(collector, target, 'using', match.index, open.at(-1)?.name ?? fileScoped);
|
|
1229
|
+
}
|
|
1230
|
+
else if (tokens.overflow) {
|
|
1231
|
+
note(collector, 'truncated', match.index, `a using directive nests its types deeper than ${MAX_EXPANSION}`);
|
|
1232
|
+
}
|
|
1233
|
+
else if (!conditional && depth === (open.at(-1)?.depth ?? 0) && startsADirective(code, match.index) && !startsAStatement(code, CSHARP_TOKEN.lastIndex)) {
|
|
1234
|
+
note(collector, 'refused', match.index, 'a directive that starts as a using is one the C# compiler refuses, and no namespace is read from it');
|
|
1235
|
+
}
|
|
656
1236
|
}
|
|
657
|
-
//
|
|
658
|
-
//
|
|
659
|
-
|
|
660
|
-
// where a terminator has to be. Earlier versions guarded those two shapes
|
|
661
|
-
// explicitly, and a third time against an empty name - none of the three
|
|
662
|
-
// changed an outcome, which mutation testing established by leaving the
|
|
663
|
-
// tests green with each of them defeated. `emit` refuses an empty
|
|
664
|
-
// specifier, so this line is the only check that has to be right.
|
|
665
|
-
if (reader.peek() !== ';')
|
|
666
|
-
continue;
|
|
667
|
-
emit(collector, target, 'using', match.index, open.at(-1)?.name ?? fileScoped);
|
|
1237
|
+
// On from where reading stopped, so that no token is read twice. No name
|
|
1238
|
+
// is `using` or `namespace`, so no directive is read into the next.
|
|
1239
|
+
CSHARP_TOKEN.lastIndex = tokens.index;
|
|
668
1240
|
}
|
|
669
1241
|
collector.namespaces = [...declared];
|
|
670
1242
|
}
|