@cplieger/deadset-ts 5.0.0 → 5.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/src/analysis.js +145 -39
- package/dist/src/configuration-files.js +108 -4
- package/dist/src/conformance-record.js +3 -9
- package/dist/src/contextual-uses.js +247 -0
- package/dist/src/convention-rows.js +6 -0
- package/dist/src/conventions.js +123 -14
- package/dist/src/dependencies.js +8 -2
- package/dist/src/destructuring.js +6 -6
- package/dist/src/entry-points.js +8 -5
- package/dist/src/exempt-classes.js +6 -1
- package/dist/src/exempt.js +35 -16
- package/dist/src/file-facts.js +23 -20
- package/dist/src/findings/completion.js +27 -3
- package/dist/src/findings/interfaces.js +6 -2
- package/dist/src/findings/reads-and-writes.js +53 -9
- package/dist/src/findings/unused-declarations.js +40 -1
- package/dist/src/generated-file.js +24 -0
- package/dist/src/graph.js +1 -1
- package/dist/src/interface-satisfaction.js +2 -68
- package/dist/src/inventory.js +17 -4
- package/dist/src/kinds.js +4 -3
- package/dist/src/manifest.js +29 -4
- package/dist/src/outside-tests.js +139 -0
- package/dist/src/overrides.js +104 -0
- package/dist/src/paths.js +17 -0
- package/dist/src/query.js +15 -0
- package/dist/src/references.js +229 -11
- package/dist/src/resolve.js +12 -11
- package/dist/src/roots.js +121 -37
- package/dist/src/session.js +4 -3
- package/dist/src/stores.js +3 -0
- package/dist/src/template-field.js +60 -3
- package/dist/src/value-flow.js +74 -0
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +194 -49
- package/src/configuration-files.ts +137 -2
- package/src/conformance-record.ts +3 -10
- package/src/contextual-uses.ts +358 -0
- package/src/convention-rows.ts +8 -0
- package/src/conventions.ts +161 -13
- package/src/dependencies.ts +14 -1
- package/src/destructuring.ts +16 -6
- package/src/entry-points.ts +16 -3
- package/src/exempt-classes.ts +6 -1
- package/src/exempt.ts +67 -16
- package/src/file-facts.ts +26 -20
- package/src/findings/completion.ts +31 -3
- package/src/findings/interfaces.ts +9 -2
- package/src/findings/reads-and-writes.ts +70 -9
- package/src/findings/unused-declarations.ts +51 -1
- package/src/generated-file.ts +27 -0
- package/src/graph.ts +4 -4
- package/src/interface-satisfaction.ts +1 -92
- package/src/inventory.ts +19 -4
- package/src/kinds.ts +5 -3
- package/src/manifest.ts +40 -4
- package/src/outside-tests.ts +191 -0
- package/src/overrides.ts +136 -0
- package/src/paths.ts +18 -0
- package/src/query.ts +26 -0
- package/src/references.ts +294 -22
- package/src/resolve.ts +14 -13
- package/src/roots.ts +152 -44
- package/src/session.ts +4 -2
- package/src/stores.ts +9 -0
- package/src/template-field.ts +65 -3
- package/src/value-flow.ts +104 -0
- package/src/version.ts +1 -1
package/src/roots.ts
CHANGED
|
@@ -8,8 +8,18 @@
|
|
|
8
8
|
* under reachability and still a candidate under reference counting.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import
|
|
11
|
+
import {
|
|
12
|
+
isExpressionWithTypeArguments,
|
|
13
|
+
isFunctionLikeDeclaration,
|
|
14
|
+
isPropertyAccessExpression,
|
|
15
|
+
isQualifiedName,
|
|
16
|
+
isTypeQueryNode,
|
|
17
|
+
isTypeReferenceNode,
|
|
18
|
+
type Node,
|
|
19
|
+
type SourceFile,
|
|
20
|
+
} from "@typescript/native/unstable/ast";
|
|
12
21
|
import { aliasChains } from "./alias-chain.ts";
|
|
22
|
+
import type { ParseFile } from "./configuration-files.ts";
|
|
13
23
|
import { conventionEntries, type Conventions } from "./conventions.ts";
|
|
14
24
|
import { entryPoints, type EntryRule } from "./entry-points.ts";
|
|
15
25
|
import { globExpression } from "./glob.ts";
|
|
@@ -87,8 +97,8 @@ export interface Roots {
|
|
|
87
97
|
readonly unmatched: readonly string[];
|
|
88
98
|
/**
|
|
89
99
|
* Every declaration of the project, where the checker left the module or export table
|
|
90
|
-
* of an entry or a published file unanswered,
|
|
91
|
-
*
|
|
100
|
+
* of an entry or a published file unanswered, since which of them that file exports is
|
|
101
|
+
* unknown; otherwise each one an unanswered published type name may name.
|
|
92
102
|
*/
|
|
93
103
|
readonly unanswered: readonly string[];
|
|
94
104
|
}
|
|
@@ -100,10 +110,10 @@ export interface RootOptions {
|
|
|
100
110
|
/** The target's own manifest. */
|
|
101
111
|
readonly manifest: Manifest;
|
|
102
112
|
/**
|
|
103
|
-
* The entry points the other packages
|
|
104
|
-
* marked published or not by its own manifest's rule.
|
|
113
|
+
* The entry points the target's other packages name, its workspace's members and the
|
|
114
|
+
* packages below it, each already marked published or not by its own manifest's rule.
|
|
105
115
|
*/
|
|
106
|
-
readonly
|
|
116
|
+
readonly otherPackages?: readonly ManifestEntry[] | undefined;
|
|
107
117
|
/** `roots.patterns` from the resolved configuration. */
|
|
108
118
|
readonly patterns: readonly string[];
|
|
109
119
|
/** `ts.entry_files` from the resolved configuration. */
|
|
@@ -112,6 +122,8 @@ export interface RootOptions {
|
|
|
112
122
|
readonly conventions?: Conventions | undefined;
|
|
113
123
|
/** `ts.test_files` from the resolved configuration. */
|
|
114
124
|
readonly testFiles: readonly string[];
|
|
125
|
+
/** The parser the configuration files no program holds are read with, where they are read. */
|
|
126
|
+
readonly parse?: ParseFile | undefined;
|
|
115
127
|
/** Whether the target is a library, so a consumer outside it reaches its published API. */
|
|
116
128
|
readonly publishedAPI: boolean;
|
|
117
129
|
}
|
|
@@ -241,7 +253,7 @@ export function roots<Brand>(
|
|
|
241
253
|
entry.published &&
|
|
242
254
|
(options.manifest.declaresExports ? entry.member.startsWith("exports") : true),
|
|
243
255
|
})),
|
|
244
|
-
...(options.
|
|
256
|
+
...(options.otherPackages ?? []).map((entry) => ({ entry, publishes: entry.published })),
|
|
245
257
|
];
|
|
246
258
|
for (const { entry, publishes } of entries) {
|
|
247
259
|
if (options.publishedAPI && publishes) {
|
|
@@ -268,7 +280,14 @@ export function roots<Brand>(
|
|
|
268
280
|
: conventionEntries(options.conventions, files)) {
|
|
269
281
|
enter(file, "convention", row, true);
|
|
270
282
|
}
|
|
271
|
-
for (const point of entryPoints(
|
|
283
|
+
for (const point of entryPoints(
|
|
284
|
+
project,
|
|
285
|
+
files,
|
|
286
|
+
options.testFiles,
|
|
287
|
+
options.host,
|
|
288
|
+
targetRoot,
|
|
289
|
+
options.parse,
|
|
290
|
+
)) {
|
|
272
291
|
enter(point.file, point.rule, point.source, point.exports);
|
|
273
292
|
}
|
|
274
293
|
for (const file of files.byPath.values()) {
|
|
@@ -300,10 +319,11 @@ export function roots<Brand>(
|
|
|
300
319
|
}
|
|
301
320
|
}
|
|
302
321
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
322
|
+
const api = options.publishedAPI
|
|
323
|
+
? publishedAPI(project, published, exportsOf, held, files.byPath)
|
|
324
|
+
: { ids: [], guessed: [] };
|
|
325
|
+
for (const id of api.ids) {
|
|
326
|
+
add(id, "published-api", "");
|
|
307
327
|
}
|
|
308
328
|
|
|
309
329
|
const unmatched = configured(held, options.patterns, add);
|
|
@@ -320,7 +340,7 @@ export function roots<Brand>(
|
|
|
320
340
|
configFile: project.configFile,
|
|
321
341
|
liveUnderReachability: ordered,
|
|
322
342
|
unmatched,
|
|
323
|
-
unanswered: answered ?
|
|
343
|
+
unanswered: answered ? api.guessed : held.symbols.map((symbol) => symbol.id),
|
|
324
344
|
};
|
|
325
345
|
}
|
|
326
346
|
|
|
@@ -378,55 +398,143 @@ function exportTables<Brand>(
|
|
|
378
398
|
const EXPORTING_CONTAINERS: ReadonlySet<string> = new Set(["file", "namespace"]);
|
|
379
399
|
|
|
380
400
|
/**
|
|
381
|
-
* Every declaration a consumer of the library can name: what the published files
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
* name. Below a published module or namespace the walk follows what its export table
|
|
388
|
-
* names; below any other published declaration it follows every member a consumer
|
|
389
|
-
* can write: a public one, and a protected one, which a consumer's subclass names. A
|
|
390
|
-
* private member is not published, nor is anything below one, and a type parameter
|
|
391
|
-
* is not published either, because a consumer supplies a type argument by position.
|
|
392
|
-
*
|
|
393
|
-
* A published file is not itself published. A consumer imports a module and names
|
|
394
|
-
* what it exports; the file is a root because the manifest names it, which is a
|
|
395
|
-
* different reason.
|
|
401
|
+
* Every declaration a consumer of the library can name: what the published files export
|
|
402
|
+
* and, below each, the members a consumer writes: a namespace's exported ones, any other
|
|
403
|
+
* declaration's public and protected ones, never a private one or a type parameter. A
|
|
404
|
+
* declaration a published one's type names is published too, exported or not. A type name
|
|
405
|
+
* the checker leaves unanswered publishes every declaration spelled as it is, and those are
|
|
406
|
+
* `guessed`. A published file is not itself published.
|
|
396
407
|
*/
|
|
397
|
-
function publishedAPI(
|
|
408
|
+
function publishedAPI<Brand>(
|
|
409
|
+
project: ProjectView<Brand>,
|
|
398
410
|
published: readonly SourceFile[],
|
|
399
411
|
exportsOf: ReadonlyMap<string, readonly string[]>,
|
|
400
412
|
held: Inventory,
|
|
401
|
-
|
|
413
|
+
byPath: ReadonlyMap<string, SourceFile>,
|
|
414
|
+
): { readonly ids: readonly string[]; readonly guessed: readonly string[] } {
|
|
402
415
|
const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
|
|
403
416
|
const children = childrenOf(held);
|
|
417
|
+
const nodes = declarationNodes(held, byPath);
|
|
418
|
+
const chains = aliasChains(project, held);
|
|
404
419
|
const reached = new Set<string>();
|
|
420
|
+
const guessed = new Set<string>();
|
|
405
421
|
const pending: string[] = [];
|
|
422
|
+
const reach = (id: string): void => {
|
|
423
|
+
if (!reached.has(id) && byId.get(id)?.kind !== "file") {
|
|
424
|
+
reached.add(id);
|
|
425
|
+
pending.push(id);
|
|
426
|
+
}
|
|
427
|
+
};
|
|
406
428
|
for (const file of published) {
|
|
407
429
|
for (const id of exportsOf.get(file.fileName) ?? []) {
|
|
408
430
|
if (byId.get(id)?.kind === "file") {
|
|
409
431
|
pending.push(id);
|
|
410
|
-
} else
|
|
411
|
-
|
|
412
|
-
pending.push(id);
|
|
432
|
+
} else {
|
|
433
|
+
reach(id);
|
|
413
434
|
}
|
|
414
435
|
}
|
|
415
436
|
}
|
|
416
|
-
|
|
417
|
-
const
|
|
418
|
-
for (
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
437
|
+
while (pending.length > 0) {
|
|
438
|
+
const round: string[] = [];
|
|
439
|
+
for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
|
|
440
|
+
round.push(next);
|
|
441
|
+
const exporting = EXPORTING_CONTAINERS.has(byId.get(next)?.kind ?? "");
|
|
442
|
+
for (const member of children.get(next) ?? []) {
|
|
443
|
+
const reachable = exporting
|
|
444
|
+
? member.exported
|
|
445
|
+
: member.visibility !== "private" && member.visibility !== "private-name";
|
|
446
|
+
if (member.kind !== "type-parameter" && reachable) {
|
|
447
|
+
reach(member.id);
|
|
448
|
+
}
|
|
424
449
|
}
|
|
425
|
-
reached.add(member.id);
|
|
426
|
-
pending.push(member.id);
|
|
427
450
|
}
|
|
451
|
+
const named = round.flatMap((id) => {
|
|
452
|
+
const symbol = byId.get(id);
|
|
453
|
+
return symbol === undefined || symbol.kind === "file" ? [] : nodes.typeNamesOf(symbol);
|
|
454
|
+
});
|
|
455
|
+
project.symbolsAt(named.map((node) => project.handle(node))).forEach((symbol, index) => {
|
|
456
|
+
if (symbol === UNANSWERED) {
|
|
457
|
+
for (const id of held.byName.get(named[index]?.getText() ?? "") ?? []) {
|
|
458
|
+
guessed.add(id);
|
|
459
|
+
reach(id);
|
|
460
|
+
}
|
|
461
|
+
} else if (symbol !== undefined) {
|
|
462
|
+
for (const target of chains.chainOf(symbol)) {
|
|
463
|
+
reach(target.id);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
});
|
|
428
467
|
}
|
|
429
|
-
return [...reached];
|
|
468
|
+
return { ids: [...reached], guessed: [...guessed] };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** The type names declarations of the inventory write, each file walked once, when first asked. */
|
|
472
|
+
function declarationNodes(
|
|
473
|
+
held: Inventory,
|
|
474
|
+
byPath: ReadonlyMap<string, SourceFile>,
|
|
475
|
+
): { typeNamesOf(symbol: InventorySymbol): readonly Node[] } {
|
|
476
|
+
const walked = new Map<string, { byId: Map<string, Node>; nodes: Set<Node> }>();
|
|
477
|
+
const nodesIn = (path: string): { byId: Map<string, Node>; nodes: Set<Node> } => {
|
|
478
|
+
const known = walked.get(path);
|
|
479
|
+
if (known !== undefined) {
|
|
480
|
+
return known;
|
|
481
|
+
}
|
|
482
|
+
const found = { byId: new Map<string, Node>(), nodes: new Set<Node>() };
|
|
483
|
+
const file = byPath.get(path);
|
|
484
|
+
if (file !== undefined) {
|
|
485
|
+
const visit = (node: Node): void => {
|
|
486
|
+
const id = held.declarations.get(nodeKey(file, node));
|
|
487
|
+
if (id !== undefined) {
|
|
488
|
+
found.nodes.add(node);
|
|
489
|
+
if (!found.byId.has(id)) {
|
|
490
|
+
found.byId.set(id, node);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
node.forEachChild(visit);
|
|
494
|
+
};
|
|
495
|
+
visit(file);
|
|
496
|
+
}
|
|
497
|
+
walked.set(path, found);
|
|
498
|
+
return found;
|
|
499
|
+
};
|
|
500
|
+
return {
|
|
501
|
+
typeNamesOf: (symbol) => {
|
|
502
|
+
const { byId, nodes } = nodesIn(symbol.position.path);
|
|
503
|
+
const declaration = byId.get(symbol.id);
|
|
504
|
+
return declaration === undefined ? [] : typeNamesOf(declaration, nodes);
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* The names of types one declaration's own syntax writes: in a type reference, a
|
|
511
|
+
* heritage clause or a type query. A function body is not read, and neither is a
|
|
512
|
+
* declaration nested in this one, which `nested` holds and which is read on its own.
|
|
513
|
+
*/
|
|
514
|
+
function typeNamesOf(declaration: Node, nested: ReadonlySet<Node>): readonly Node[] {
|
|
515
|
+
const found: Node[] = [];
|
|
516
|
+
const visit = (node: Node): void => {
|
|
517
|
+
if (node !== declaration && nested.has(node)) {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (isTypeReferenceNode(node)) {
|
|
521
|
+
found.push(isQualifiedName(node.typeName) ? node.typeName.right : node.typeName);
|
|
522
|
+
} else if (isExpressionWithTypeArguments(node)) {
|
|
523
|
+
found.push(
|
|
524
|
+
isPropertyAccessExpression(node.expression) ? node.expression.name : node.expression,
|
|
525
|
+
);
|
|
526
|
+
} else if (isTypeQueryNode(node)) {
|
|
527
|
+
found.push(isQualifiedName(node.exprName) ? node.exprName.right : node.exprName);
|
|
528
|
+
}
|
|
529
|
+
const body = isFunctionLikeDeclaration(node) ? node.body : undefined;
|
|
530
|
+
node.forEachChild((child) => {
|
|
531
|
+
if (child !== body) {
|
|
532
|
+
visit(child);
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
};
|
|
536
|
+
visit(declaration);
|
|
537
|
+
return found;
|
|
430
538
|
}
|
|
431
539
|
|
|
432
540
|
/**
|
package/src/session.ts
CHANGED
|
@@ -485,8 +485,8 @@ function projectsOf(
|
|
|
485
485
|
* snapshot of one client, and answers in their order. A configuration `programs` describes is
|
|
486
486
|
* built as that program, or opened from the configuration it writes, instead of as a project.
|
|
487
487
|
* One the snapshot holds nothing for ends the run under `"refuse"` and is named in the result
|
|
488
|
-
* under `"record"`.
|
|
489
|
-
* after it, whether the visit completed or threw.
|
|
488
|
+
* under `"record"`. `last` runs after the last visit while the client still answers; the
|
|
489
|
+
* snapshot is disposed and the client released after it, whether the visit completed or threw.
|
|
490
490
|
*/
|
|
491
491
|
export function runSession<Result>(
|
|
492
492
|
engine: Engine,
|
|
@@ -494,6 +494,7 @@ export function runSession<Result>(
|
|
|
494
494
|
visit: ProjectVisitor<Result>,
|
|
495
495
|
unopened: Unopened = "refuse",
|
|
496
496
|
programs: ReadonlyMap<string, WorkspaceProgram> = new Map(),
|
|
497
|
+
last: () => void = () => undefined,
|
|
497
498
|
): SessionResult<Result> {
|
|
498
499
|
try {
|
|
499
500
|
const built: string[] = [];
|
|
@@ -531,6 +532,7 @@ export function runSession<Result>(
|
|
|
531
532
|
}
|
|
532
533
|
projects.push(visit(viewOf(project, configFile, engine, log)));
|
|
533
534
|
}
|
|
535
|
+
last();
|
|
534
536
|
return {
|
|
535
537
|
projects,
|
|
536
538
|
unopened: missing,
|
package/src/stores.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface Stores {
|
|
|
22
22
|
* configuration. A production mode counts none a test file made.
|
|
23
23
|
*/
|
|
24
24
|
readonly references: readonly Reference[];
|
|
25
|
+
/** The declarations a test file reads, which a production mode counts no read of. */
|
|
26
|
+
readonly readInTests: ReadonlySet<string>;
|
|
25
27
|
/**
|
|
26
28
|
* The class members declared by a getter or a setter. A store into one runs the
|
|
27
29
|
* setter, which is code rather than state.
|
|
@@ -71,6 +73,13 @@ export function storesOf(
|
|
|
71
73
|
references: per.flatMap((one) =>
|
|
72
74
|
one.references.filter((reference) => !mode.production || !reference.test),
|
|
73
75
|
),
|
|
76
|
+
readInTests: new Set(
|
|
77
|
+
per.flatMap((one) =>
|
|
78
|
+
one.references
|
|
79
|
+
.filter((reference) => reference.test && reference.use !== "write")
|
|
80
|
+
.map((reference) => reference.to),
|
|
81
|
+
),
|
|
82
|
+
),
|
|
74
83
|
accessors: new Set(per.flatMap((one) => one.accessors)),
|
|
75
84
|
exempt: new Set(exempt.map((record) => record.id)),
|
|
76
85
|
};
|
package/src/template-field.ts
CHANGED
|
@@ -125,12 +125,74 @@ function stringEnd(text: string, from: number): number {
|
|
|
125
125
|
return at + 1;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
/** The
|
|
129
|
-
function
|
|
128
|
+
/** The end of the `${` substitution whose expression starts at `from`, past its closing brace. */
|
|
129
|
+
function substitutionEnd(text: string, from: number): number {
|
|
130
|
+
let depth = 1;
|
|
131
|
+
let at = from;
|
|
132
|
+
while (at < text.length) {
|
|
133
|
+
const char = text[at];
|
|
134
|
+
if (char === '"' || char === "'") {
|
|
135
|
+
at = stringEnd(text, at);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (char === "`") {
|
|
139
|
+
at = templateEnd(text, at, () => undefined);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (char === "{") {
|
|
143
|
+
depth += 1;
|
|
144
|
+
} else if (char === "}") {
|
|
145
|
+
depth -= 1;
|
|
146
|
+
if (depth === 0) {
|
|
147
|
+
return at + 1;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
at += 1;
|
|
151
|
+
}
|
|
152
|
+
return at;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The end of the template literal at `from`; each `${…}` expression goes to `substitution`. */
|
|
156
|
+
function templateEnd(
|
|
157
|
+
text: string,
|
|
158
|
+
from: number,
|
|
159
|
+
substitution: (expression: string, offset: number) => void,
|
|
160
|
+
): number {
|
|
161
|
+
let at = from + 1;
|
|
162
|
+
while (at < text.length && text[at] !== "`") {
|
|
163
|
+
if (text[at] === "\\") {
|
|
164
|
+
at += 2;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (text[at] === "$" && text[at + 1] === "{") {
|
|
168
|
+
const start = at + 2;
|
|
169
|
+
const end = substitutionEnd(text, start);
|
|
170
|
+
substitution(text.slice(start, end - 1), start);
|
|
171
|
+
at = end;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
at += 1;
|
|
175
|
+
}
|
|
176
|
+
return at + 1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The identifiers one action's inner text writes outside a quoted string, with their offsets
|
|
181
|
+
* in it. In `script` text, a component's markup, a template literal's substitutions are code.
|
|
182
|
+
*/
|
|
183
|
+
function identifiersOf(inner: string, script = false): readonly { name: string; offset: number }[] {
|
|
130
184
|
const found: { name: string; offset: number }[] = [];
|
|
131
185
|
let at = 0;
|
|
132
186
|
while (at < inner.length) {
|
|
133
187
|
const char = inner[at] ?? "";
|
|
188
|
+
if (char === "`" && script) {
|
|
189
|
+
at = templateEnd(inner, at, (expression, offset) => {
|
|
190
|
+
for (const one of identifiersOf(expression, true)) {
|
|
191
|
+
found.push({ name: one.name, offset: offset + one.offset });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
134
196
|
if (char === '"' || char === "'" || char === "`") {
|
|
135
197
|
at = stringEnd(inner, at);
|
|
136
198
|
continue;
|
|
@@ -183,7 +245,7 @@ function namesOf(text: string, delimiters: TemplateDelimiters): readonly Named[]
|
|
|
183
245
|
/** Every identifier the markup of one component file writes in an action. */
|
|
184
246
|
function markupNamesOf(text: string): readonly Named[] {
|
|
185
247
|
return readComponent(text).markup.flatMap((one) =>
|
|
186
|
-
identifiersOf(one.text).map(({ name, offset }) => ({
|
|
248
|
+
identifiersOf(one.text, true).map(({ name, offset }) => ({
|
|
187
249
|
name,
|
|
188
250
|
offset: one.start + offset,
|
|
189
251
|
action: one.action,
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The values a program hands to a position the checker types from its context: the
|
|
3
|
+
* initializer of a declaration whose type is written, an assignment's right side, an
|
|
4
|
+
* argument, a returned value, an arrow function's expression body, an array element, an
|
|
5
|
+
* object property, and the operand of `as` or `satisfies`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
isArrayLiteralExpression,
|
|
10
|
+
isArrowFunction,
|
|
11
|
+
isAsExpression,
|
|
12
|
+
isBinaryExpression,
|
|
13
|
+
isCallExpression,
|
|
14
|
+
isConditionalExpression,
|
|
15
|
+
isIdentifier,
|
|
16
|
+
isNewExpression,
|
|
17
|
+
isParameterDeclaration,
|
|
18
|
+
isParenthesizedExpression,
|
|
19
|
+
isPropertyAssignment,
|
|
20
|
+
isPropertyDeclaration,
|
|
21
|
+
isReturnStatement,
|
|
22
|
+
isSatisfiesExpression,
|
|
23
|
+
isShorthandPropertyAssignment,
|
|
24
|
+
isSpreadElement,
|
|
25
|
+
isVariableDeclaration,
|
|
26
|
+
SyntaxKind,
|
|
27
|
+
type Expression,
|
|
28
|
+
type Node,
|
|
29
|
+
} from "@typescript/native/unstable/ast";
|
|
30
|
+
|
|
31
|
+
/** The operators of an assignment whose right side is stored into the left side's type. */
|
|
32
|
+
const STORES: ReadonlySet<SyntaxKind> = new Set([
|
|
33
|
+
SyntaxKind.EqualsToken,
|
|
34
|
+
SyntaxKind.QuestionQuestionEqualsToken,
|
|
35
|
+
SyntaxKind.BarBarEqualsToken,
|
|
36
|
+
SyntaxKind.AmpersandAmpersandEqualsToken,
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
/** Whether a declaration writes its type, so its initializer is read in that type's context. */
|
|
40
|
+
function writesType(node: Node): boolean {
|
|
41
|
+
return (node as { readonly type?: Node }).type !== undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The values one written expression hands on: the expression itself, or what it
|
|
46
|
+
* evaluates to through parentheses, a conditional and the operators whose result is
|
|
47
|
+
* one of their operands.
|
|
48
|
+
*/
|
|
49
|
+
export function valuesOf(expression: Expression): readonly Expression[] {
|
|
50
|
+
if (isParenthesizedExpression(expression)) {
|
|
51
|
+
return valuesOf(expression.expression);
|
|
52
|
+
}
|
|
53
|
+
if (isConditionalExpression(expression)) {
|
|
54
|
+
return [...valuesOf(expression.whenTrue), ...valuesOf(expression.whenFalse)];
|
|
55
|
+
}
|
|
56
|
+
if (isBinaryExpression(expression)) {
|
|
57
|
+
const operator = expression.operatorToken.kind;
|
|
58
|
+
if (operator === SyntaxKind.QuestionQuestionToken || operator === SyntaxKind.BarBarToken) {
|
|
59
|
+
return [...valuesOf(expression.left), ...valuesOf(expression.right)];
|
|
60
|
+
}
|
|
61
|
+
if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.CommaToken) {
|
|
62
|
+
return valuesOf(expression.right);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return [expression];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The expressions one node hands to a position typed by its context. */
|
|
69
|
+
export function handedOn(node: Node): readonly Expression[] {
|
|
70
|
+
if (
|
|
71
|
+
(isVariableDeclaration(node) || isPropertyDeclaration(node) || isParameterDeclaration(node)) &&
|
|
72
|
+
node.initializer !== undefined &&
|
|
73
|
+
writesType(node)
|
|
74
|
+
) {
|
|
75
|
+
return [node.initializer];
|
|
76
|
+
}
|
|
77
|
+
if (isBinaryExpression(node) && STORES.has(node.operatorToken.kind)) {
|
|
78
|
+
return [node.right];
|
|
79
|
+
}
|
|
80
|
+
if (isCallExpression(node) || isNewExpression(node)) {
|
|
81
|
+
return (node.arguments ?? []).filter((argument) => !isSpreadElement(argument));
|
|
82
|
+
}
|
|
83
|
+
if (isReturnStatement(node)) {
|
|
84
|
+
return node.expression === undefined ? [] : [node.expression];
|
|
85
|
+
}
|
|
86
|
+
if (isArrowFunction(node)) {
|
|
87
|
+
return node.body.kind === SyntaxKind.Block ? [] : [node.body];
|
|
88
|
+
}
|
|
89
|
+
if (isArrayLiteralExpression(node)) {
|
|
90
|
+
return node.elements.filter(
|
|
91
|
+
(element) => !isSpreadElement(element) && element.kind !== SyntaxKind.OmittedExpression,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (isPropertyAssignment(node)) {
|
|
95
|
+
return [node.initializer];
|
|
96
|
+
}
|
|
97
|
+
if (isShorthandPropertyAssignment(node)) {
|
|
98
|
+
return isIdentifier(node.name) ? [node.name] : [];
|
|
99
|
+
}
|
|
100
|
+
if (isAsExpression(node) || isSatisfiesExpression(node)) {
|
|
101
|
+
return [node.expression];
|
|
102
|
+
}
|
|
103
|
+
return [];
|
|
104
|
+
}
|
package/src/version.ts
CHANGED