@cplieger/deadset-ts 5.1.1 → 5.2.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/dist/src/analysis.js +7 -4
- package/dist/src/config.js +3 -0
- package/dist/src/configuration-files.js +23 -24
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/contextual-uses.js +25 -4
- package/dist/src/convention-rows.js +7 -0
- package/dist/src/conventions.js +97 -33
- package/dist/src/dependencies.js +8 -0
- package/dist/src/entry-point-gaps.js +10 -26
- package/dist/src/entry-points.js +4 -0
- package/dist/src/findings/completion.js +27 -11
- package/dist/src/findings/interfaces.js +2 -6
- package/dist/src/findings/intra-function.js +15 -3
- package/dist/src/findings/unused-declarations.js +5 -6
- package/dist/src/findings/visibility-narrowing.js +6 -1
- package/dist/src/generated-file.js +72 -12
- package/dist/src/graph.js +5 -4
- package/dist/src/html-entries.js +137 -0
- package/dist/src/kinds.js +2 -2
- package/dist/src/manifest.js +5 -1
- package/dist/src/matrix.js +9 -3
- package/dist/src/namespace-objects.js +158 -0
- package/dist/src/partial.js +1 -1
- package/dist/src/references.js +120 -3
- package/dist/src/roots.js +52 -3
- package/dist/src/sweep.js +17 -0
- package/dist/src/test-support.js +35 -23
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +12 -5
- package/src/config.ts +3 -0
- package/src/configuration-files.ts +28 -24
- package/src/conformance-record.ts +2 -2
- package/src/contextual-uses.ts +31 -4
- package/src/convention-rows.ts +15 -0
- package/src/conventions.ts +119 -30
- package/src/dependencies.ts +9 -0
- package/src/entry-point-gaps.ts +11 -28
- package/src/entry-points.ts +7 -1
- package/src/exempt.ts +1 -4
- package/src/file-facts.ts +1 -1
- package/src/findings/completion.ts +31 -11
- package/src/findings/interfaces.ts +2 -9
- package/src/findings/intra-function.ts +17 -3
- package/src/findings/unused-declarations.ts +5 -8
- package/src/findings/visibility-narrowing.ts +7 -1
- package/src/generated-file.ts +80 -12
- package/src/graph.ts +5 -4
- package/src/html-entries.ts +170 -0
- package/src/kinds.ts +2 -2
- package/src/manifest.ts +9 -3
- package/src/matrix.ts +21 -3
- package/src/namespace-objects.ts +216 -0
- package/src/partial.ts +1 -1
- package/src/references.ts +161 -3
- package/src/roots.ts +69 -10
- package/src/sweep.ts +19 -0
- package/src/test-support.ts +34 -22
- package/src/version.ts +1 -1
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module namespace objects: the binding of a namespace import, the value an `await` of a
|
|
3
|
+
* dynamic `import()` produces, the first parameter of the first callback passed to `then`
|
|
4
|
+
* on one, and a `const` binding initialized with one of these. A use reads the exports it
|
|
5
|
+
* names and no other: a property access by name or by a literal index, or a destructured
|
|
6
|
+
* property, names one export, and any other use may read every export.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
isArrowFunction,
|
|
11
|
+
isAwaitExpression,
|
|
12
|
+
isBindingElement,
|
|
13
|
+
isCallExpression,
|
|
14
|
+
isElementAccessExpression,
|
|
15
|
+
isExpressionStatement,
|
|
16
|
+
isFunctionExpression,
|
|
17
|
+
isIdentifier,
|
|
18
|
+
isImportDeclaration,
|
|
19
|
+
isNamespaceImport,
|
|
20
|
+
isNoSubstitutionTemplateLiteral,
|
|
21
|
+
isObjectBindingPattern,
|
|
22
|
+
isParenthesizedExpression,
|
|
23
|
+
isPropertyAccessExpression,
|
|
24
|
+
isQualifiedName,
|
|
25
|
+
isStringLiteral,
|
|
26
|
+
isVariableDeclaration,
|
|
27
|
+
isVariableDeclarationList,
|
|
28
|
+
isVoidExpression,
|
|
29
|
+
NodeFlags,
|
|
30
|
+
SyntaxKind,
|
|
31
|
+
type Identifier,
|
|
32
|
+
type Node,
|
|
33
|
+
type SourceFile,
|
|
34
|
+
} from "@typescript/native/unstable/ast";
|
|
35
|
+
|
|
36
|
+
/** What one use of a namespace object reads beyond the names the checker resolves. */
|
|
37
|
+
export type NamespaceUse =
|
|
38
|
+
/** Every export of the module `specifier` names. */
|
|
39
|
+
| { readonly kind: "whole"; readonly specifier: Node }
|
|
40
|
+
/** The export a literal index names, the literal being the node to resolve. */
|
|
41
|
+
| { readonly kind: "indexed"; readonly literal: Node };
|
|
42
|
+
|
|
43
|
+
function isLiteralText(node: Node): boolean {
|
|
44
|
+
return isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The outermost expression one node is, through the parentheses written around it. */
|
|
48
|
+
function parenthesized(node: Node): Node {
|
|
49
|
+
let at = node;
|
|
50
|
+
while (isParenthesizedExpression(at.parent)) {
|
|
51
|
+
at = at.parent;
|
|
52
|
+
}
|
|
53
|
+
return at;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Whether one call is a dynamic `import()` whose argument is literal text. */
|
|
57
|
+
function isLiteralImport(node: Node): boolean {
|
|
58
|
+
if (!isCallExpression(node) || node.expression.kind !== SyntaxKind.ImportKeyword) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
const [argument] = node.arguments;
|
|
62
|
+
return argument !== undefined && isLiteralText(argument);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The node a `const` binding's name is visible below: the block or file its statement is in. */
|
|
66
|
+
function scopeOf(declaration: Node): Node {
|
|
67
|
+
const statement = declaration.parent.parent;
|
|
68
|
+
return statement.parent;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Every identifier below `scope` that spells `name`, other than `declared` and a member's name. */
|
|
72
|
+
function occurrences(scope: Node, name: string, declared: Node): readonly Identifier[] {
|
|
73
|
+
const found: Identifier[] = [];
|
|
74
|
+
const visit = (node: Node): void => {
|
|
75
|
+
if (isIdentifier(node)) {
|
|
76
|
+
const parent = node.parent;
|
|
77
|
+
const member =
|
|
78
|
+
(isPropertyAccessExpression(parent) && parent.name === node) ||
|
|
79
|
+
(isQualifiedName(parent) && parent.right === node) ||
|
|
80
|
+
(isBindingElement(parent) && parent.propertyName === node);
|
|
81
|
+
if (node !== declared && node.text === name && !member) {
|
|
82
|
+
found.push(node);
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
node.forEachChild(visit);
|
|
87
|
+
};
|
|
88
|
+
scope.forEachChild(visit);
|
|
89
|
+
return found;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Every use of the namespace objects one file holds, keyed by the node the use is written
|
|
94
|
+
* at, that reads more than a name the checker resolves: an indexed read by a literal, and
|
|
95
|
+
* any use that may read every export. A shadowing declaration of a binding's name inside
|
|
96
|
+
* its scope is read as the binding, which can only keep an export live.
|
|
97
|
+
*/
|
|
98
|
+
export function namespaceUses(file: SourceFile): ReadonlyMap<Node, NamespaceUse> {
|
|
99
|
+
const uses = new Map<Node, NamespaceUse>();
|
|
100
|
+
|
|
101
|
+
const binding = (name: Identifier, scope: Node, specifier: Node, seen: Set<Node>): void => {
|
|
102
|
+
if (seen.has(name)) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
seen.add(name);
|
|
106
|
+
for (const one of occurrences(scope, name.text, name)) {
|
|
107
|
+
value(one, specifier, seen);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** One use of a namespace object's value, at `node`. */
|
|
112
|
+
const value = (node: Node, specifier: Node, seen: Set<Node>): void => {
|
|
113
|
+
const at = parenthesized(node);
|
|
114
|
+
const parent = at.parent;
|
|
115
|
+
if (isExpressionStatement(parent) || isVoidExpression(parent)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (
|
|
119
|
+
(isPropertyAccessExpression(parent) && parent.expression === at) ||
|
|
120
|
+
(isQualifiedName(parent) && parent.left === at)
|
|
121
|
+
) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (isElementAccessExpression(parent) && parent.expression === at) {
|
|
125
|
+
const index = parent.argumentExpression;
|
|
126
|
+
uses.set(
|
|
127
|
+
node,
|
|
128
|
+
isLiteralText(index) ? { kind: "indexed", literal: index } : { kind: "whole", specifier },
|
|
129
|
+
);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (isVariableDeclaration(parent) && parent.initializer === at) {
|
|
133
|
+
const pattern = parent.name;
|
|
134
|
+
if (isObjectBindingPattern(pattern)) {
|
|
135
|
+
const named = pattern.elements.every(
|
|
136
|
+
(element) =>
|
|
137
|
+
element.dotDotDotToken === undefined &&
|
|
138
|
+
element.propertyName?.kind !== SyntaxKind.ComputedPropertyName,
|
|
139
|
+
);
|
|
140
|
+
if (!named) {
|
|
141
|
+
uses.set(node, { kind: "whole", specifier });
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const list = parent.parent;
|
|
146
|
+
if (
|
|
147
|
+
isIdentifier(pattern) &&
|
|
148
|
+
isVariableDeclarationList(list) &&
|
|
149
|
+
(list.flags & NodeFlags.Const) !== 0
|
|
150
|
+
) {
|
|
151
|
+
binding(pattern, scopeOf(parent), specifier, seen);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
uses.set(node, { kind: "whole", specifier });
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/** One use of a dynamic `import()` call, at `call`. */
|
|
159
|
+
const imported = (call: Node, specifier: Node, seen: Set<Node>): void => {
|
|
160
|
+
const at = parenthesized(call);
|
|
161
|
+
const parent = at.parent;
|
|
162
|
+
if (isExpressionStatement(parent) || isVoidExpression(parent)) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (isAwaitExpression(parent)) {
|
|
166
|
+
value(parent, specifier, seen);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const then = parent.parent;
|
|
170
|
+
if (
|
|
171
|
+
isPropertyAccessExpression(parent) &&
|
|
172
|
+
parent.expression === at &&
|
|
173
|
+
parent.name.text === "then" &&
|
|
174
|
+
isCallExpression(then) &&
|
|
175
|
+
then.expression === parent
|
|
176
|
+
) {
|
|
177
|
+
const [callback] = then.arguments;
|
|
178
|
+
if (callback !== undefined && (isArrowFunction(callback) || isFunctionExpression(callback))) {
|
|
179
|
+
const [first] = callback.parameters;
|
|
180
|
+
if (first === undefined || isObjectBindingPattern(first.name)) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (isIdentifier(first.name)) {
|
|
184
|
+
binding(first.name, callback, specifier, seen);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
uses.set(call, { kind: "whole", specifier });
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const visit = (node: Node): void => {
|
|
193
|
+
if (isImportDeclaration(node)) {
|
|
194
|
+
const clause = node.importClause;
|
|
195
|
+
const bindings = clause?.namedBindings;
|
|
196
|
+
if (
|
|
197
|
+
clause !== undefined &&
|
|
198
|
+
clause.phaseModifier !== SyntaxKind.TypeKeyword &&
|
|
199
|
+
bindings !== undefined &&
|
|
200
|
+
isNamespaceImport(bindings)
|
|
201
|
+
) {
|
|
202
|
+
binding(bindings.name, file, node.moduleSpecifier, new Set());
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (isLiteralImport(node) && isCallExpression(node)) {
|
|
207
|
+
const [argument] = node.arguments;
|
|
208
|
+
if (argument !== undefined) {
|
|
209
|
+
imported(node, argument, new Set());
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
node.forEachChild(visit);
|
|
213
|
+
};
|
|
214
|
+
file.forEachChild(visit);
|
|
215
|
+
return uses;
|
|
216
|
+
}
|
package/src/partial.ts
CHANGED
|
@@ -38,7 +38,7 @@ export function partialNotes(partial: Partial): readonly string[] {
|
|
|
38
38
|
}
|
|
39
39
|
const where = [...byProject].map(([configFile, n]) => `${configFile}: ${String(n)}`).join(", ");
|
|
40
40
|
notes.push(
|
|
41
|
-
`the checker answered ${counted(partial.unanswered.length, "question", "questions")} with a failure (${where})
|
|
41
|
+
`the checker answered ${counted(partial.unanswered.length, "question", "questions")} with a failure (${where}), so every declaration an answer could have kept live is kept live and reported by nothing`,
|
|
42
42
|
);
|
|
43
43
|
}
|
|
44
44
|
return notes;
|
package/src/references.ts
CHANGED
|
@@ -12,8 +12,11 @@ import {
|
|
|
12
12
|
isForInStatement,
|
|
13
13
|
isForOfStatement,
|
|
14
14
|
isForStatement,
|
|
15
|
+
isIdentifier,
|
|
15
16
|
isImportDeclaration,
|
|
16
17
|
isImportEqualsDeclaration,
|
|
18
|
+
isMetaProperty,
|
|
19
|
+
isNewExpression,
|
|
17
20
|
isNamespaceExport,
|
|
18
21
|
isNoSubstitutionTemplateLiteral,
|
|
19
22
|
isObjectLiteralExpression,
|
|
@@ -35,6 +38,8 @@ import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
|
|
|
35
38
|
import { aliasChains, type ChainTarget } from "./alias-chain.ts";
|
|
36
39
|
import { contextualUses, type FlowAt, type LiteralAt } from "./contextual-uses.ts";
|
|
37
40
|
import { destructuring, propertyReadsOf, type PropertyRead } from "./destructuring.ts";
|
|
41
|
+
import { specifierCandidates } from "./configuration-files.ts";
|
|
42
|
+
import { namespaceUses } from "./namespace-objects.ts";
|
|
38
43
|
import { globExpression } from "./glob.ts";
|
|
39
44
|
import { declarationsByName, nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
|
|
40
45
|
import { overrideReferences } from "./overrides.ts";
|
|
@@ -424,6 +429,27 @@ interface Evaluation {
|
|
|
424
429
|
readonly from: string;
|
|
425
430
|
}
|
|
426
431
|
|
|
432
|
+
/**
|
|
433
|
+
* One module URL a source file builds: `new URL(<literal>, import.meta.url)` or
|
|
434
|
+
* `import.meta.resolve(<literal>)`, the literal relative. A runtime starts the file it
|
|
435
|
+
* names as a module, so it is an import of that file for its effects.
|
|
436
|
+
*/
|
|
437
|
+
interface ModuleURL {
|
|
438
|
+
readonly specifier: string;
|
|
439
|
+
/** The `URL` the constructor names, which must name no declaration of the target. */
|
|
440
|
+
readonly constructor: Node | undefined;
|
|
441
|
+
readonly at: Node;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** One use of a module namespace object that may read every export of the module. */
|
|
445
|
+
interface WholeRead {
|
|
446
|
+
/** The module specifier, whose symbol is the module read. */
|
|
447
|
+
readonly specifier: Node;
|
|
448
|
+
/** The use, whose position the reference carries. */
|
|
449
|
+
readonly at: Node;
|
|
450
|
+
readonly from: string;
|
|
451
|
+
}
|
|
452
|
+
|
|
427
453
|
/** One decorator expression, and the declaration it is attached to. */
|
|
428
454
|
interface Decorated {
|
|
429
455
|
readonly decorator: Node;
|
|
@@ -443,9 +469,55 @@ interface FileSites {
|
|
|
443
469
|
readonly reads: readonly Read[];
|
|
444
470
|
readonly links: readonly Link[];
|
|
445
471
|
readonly evaluations: readonly Evaluation[];
|
|
472
|
+
readonly wholes: readonly WholeRead[];
|
|
473
|
+
readonly urls: readonly ModuleURL[];
|
|
446
474
|
readonly decorated: readonly Decorated[];
|
|
447
475
|
}
|
|
448
476
|
|
|
477
|
+
/** Whether one node is `import.meta.url`. */
|
|
478
|
+
function isImportMetaURL(node: Node): boolean {
|
|
479
|
+
const at = unparenthesized(node);
|
|
480
|
+
return (
|
|
481
|
+
isPropertyAccessExpression(at) &&
|
|
482
|
+
isMetaProperty(at.expression) &&
|
|
483
|
+
at.expression.keywordToken === SyntaxKind.ImportKeyword &&
|
|
484
|
+
at.name.text === "url"
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** The module URL one node builds, where it builds one. */
|
|
489
|
+
function moduleURLOf(node: Node): ModuleURL | undefined {
|
|
490
|
+
const relative = (argument: Node | undefined): string | undefined => {
|
|
491
|
+
const at = argument === undefined ? undefined : unparenthesized(argument);
|
|
492
|
+
return at !== undefined &&
|
|
493
|
+
(isStringLiteral(at) || isNoSubstitutionTemplateLiteral(at)) &&
|
|
494
|
+
/^\.\.?\//u.test(at.text)
|
|
495
|
+
? at.text
|
|
496
|
+
: undefined;
|
|
497
|
+
};
|
|
498
|
+
if (isNewExpression(node) && isIdentifier(node.expression) && node.expression.text === "URL") {
|
|
499
|
+
const [reference, base] = node.arguments ?? [];
|
|
500
|
+
const specifier = relative(reference);
|
|
501
|
+
return specifier !== undefined && base !== undefined && isImportMetaURL(base)
|
|
502
|
+
? { specifier, constructor: node.expression, at: node }
|
|
503
|
+
: undefined;
|
|
504
|
+
}
|
|
505
|
+
if (isCallExpression(node)) {
|
|
506
|
+
const callee = unparenthesized(node.expression);
|
|
507
|
+
const specifier = relative(node.arguments[0]);
|
|
508
|
+
if (
|
|
509
|
+
specifier !== undefined &&
|
|
510
|
+
isPropertyAccessExpression(callee) &&
|
|
511
|
+
isMetaProperty(callee.expression) &&
|
|
512
|
+
callee.expression.keywordToken === SyntaxKind.ImportKeyword &&
|
|
513
|
+
callee.name.text === "resolve"
|
|
514
|
+
) {
|
|
515
|
+
return { specifier, constructor: undefined, at: node };
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return undefined;
|
|
519
|
+
}
|
|
520
|
+
|
|
449
521
|
/** Whether one node is a string literal or a template literal with no substitution. */
|
|
450
522
|
function isLiteralText(node: Node | undefined): node is Node {
|
|
451
523
|
return node !== undefined && (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node));
|
|
@@ -584,7 +656,10 @@ function sitesOf(
|
|
|
584
656
|
const reads: Read[] = [];
|
|
585
657
|
const links: Link[] = [];
|
|
586
658
|
const evaluations: Evaluation[] = [];
|
|
659
|
+
const wholes: WholeRead[] = [];
|
|
660
|
+
const urls: ModuleURL[] = [];
|
|
587
661
|
const decorated: Decorated[] = [];
|
|
662
|
+
const namespaces = namespaceUses(file);
|
|
588
663
|
const literals: LiteralAt[] = [];
|
|
589
664
|
const flows: FlowAt[] = [];
|
|
590
665
|
const writes = new Set<number>();
|
|
@@ -615,6 +690,12 @@ function sitesOf(
|
|
|
615
690
|
if (unread.has(node)) {
|
|
616
691
|
return;
|
|
617
692
|
}
|
|
693
|
+
const namespace = namespaces.get(node);
|
|
694
|
+
if (namespace?.kind === "whole") {
|
|
695
|
+
wholes.push({ specifier: namespace.specifier, at: node, from: enclosing });
|
|
696
|
+
} else if (namespace?.kind === "indexed") {
|
|
697
|
+
found.push({ node: namespace.literal, shorthand: undefined, from: enclosing, use: "read" });
|
|
698
|
+
}
|
|
618
699
|
if (isName(node)) {
|
|
619
700
|
if (!declared.has(node.pos)) {
|
|
620
701
|
const site = { node, shorthand: shorthands.get(node.pos), from: enclosing };
|
|
@@ -644,6 +725,10 @@ function sitesOf(
|
|
|
644
725
|
if (evaluated !== undefined) {
|
|
645
726
|
evaluations.push({ node: evaluated, from: enclosing });
|
|
646
727
|
}
|
|
728
|
+
const url = moduleURLOf(node);
|
|
729
|
+
if (url !== undefined) {
|
|
730
|
+
urls.push(url);
|
|
731
|
+
}
|
|
647
732
|
const held = declarations.get(nodeKey(file, node));
|
|
648
733
|
const outer = enclosing;
|
|
649
734
|
if (held !== undefined) {
|
|
@@ -659,7 +744,7 @@ function sitesOf(
|
|
|
659
744
|
};
|
|
660
745
|
|
|
661
746
|
file.forEachChild(visit);
|
|
662
|
-
return { uses: found, literals, flows, reads, links, evaluations, decorated };
|
|
747
|
+
return { uses: found, literals, flows, reads, links, evaluations, wholes, urls, decorated };
|
|
663
748
|
}
|
|
664
749
|
|
|
665
750
|
/** The declaration kinds of a property an object literal declares. */
|
|
@@ -922,6 +1007,36 @@ export function references<Brand>(
|
|
|
922
1007
|
return topLevel.get(fileId) ?? [];
|
|
923
1008
|
};
|
|
924
1009
|
|
|
1010
|
+
const exportsCache = new Map<number, readonly ChainTarget[]>();
|
|
1011
|
+
/**
|
|
1012
|
+
* Every declaration one module exports, each re-export walked along its chain. A module
|
|
1013
|
+
* the checker left unanswered may be any file, so it reads every file's exports.
|
|
1014
|
+
*/
|
|
1015
|
+
const exportsRead = (module: Answer<TSSymbol>): readonly ChainTarget[] => {
|
|
1016
|
+
if (module === UNANSWERED) {
|
|
1017
|
+
return [...fileIds].flatMap((id) =>
|
|
1018
|
+
declaredIn(id)
|
|
1019
|
+
.filter((symbol) => symbol.exported)
|
|
1020
|
+
.map((symbol) => ({ id: symbol.id, stepped: false, guessed: true })),
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
const known = exportsCache.get(module.id);
|
|
1024
|
+
if (known !== undefined) {
|
|
1025
|
+
return known;
|
|
1026
|
+
}
|
|
1027
|
+
const table = project.queries.exportsOfModule(module);
|
|
1028
|
+
const read =
|
|
1029
|
+
table === UNANSWERED
|
|
1030
|
+
? chains.declarationsOf(module).flatMap((id) =>
|
|
1031
|
+
declaredIn(id)
|
|
1032
|
+
.filter((symbol) => symbol.exported)
|
|
1033
|
+
.map((symbol) => ({ id: symbol.id, stepped: false, guessed: true })),
|
|
1034
|
+
)
|
|
1035
|
+
: table.flatMap((exported) => chains.chainOf(exported));
|
|
1036
|
+
exportsCache.set(module.id, read);
|
|
1037
|
+
return read;
|
|
1038
|
+
};
|
|
1039
|
+
|
|
925
1040
|
/** The identifier of the declaration one node of this program is, where it is one. */
|
|
926
1041
|
const declaredAt = (node: Node): string | undefined =>
|
|
927
1042
|
declarations.get(nodeKey(node.getSourceFile(), node));
|
|
@@ -1022,7 +1137,7 @@ export function references<Brand>(
|
|
|
1022
1137
|
const fileId = declaredAt(file) ?? "";
|
|
1023
1138
|
return { file, fileId, ...sitesOf(file, declarations, fileId, unread) };
|
|
1024
1139
|
});
|
|
1025
|
-
const fileIdByName = new Map(
|
|
1140
|
+
const fileIdByName = new Map<string, string>(
|
|
1026
1141
|
walked.filter((one) => fileIds.has(one.fileId)).map((one) => [one.file.fileName, one.fileId]),
|
|
1027
1142
|
);
|
|
1028
1143
|
const contextual = contextualUses(
|
|
@@ -1112,7 +1227,7 @@ export function references<Brand>(
|
|
|
1112
1227
|
}
|
|
1113
1228
|
}
|
|
1114
1229
|
|
|
1115
|
-
for (const { file, fileId, uses, reads, links, evaluations, decorated } of walked) {
|
|
1230
|
+
for (const { file, fileId, uses, reads, links, evaluations, wholes, urls, decorated } of walked) {
|
|
1116
1231
|
const test = testFile.get(file) === true;
|
|
1117
1232
|
// A component file's markup may use any binding its blocks declare at the top level.
|
|
1118
1233
|
const markup = isComponentFile(file);
|
|
@@ -1142,6 +1257,8 @@ export function references<Brand>(
|
|
|
1142
1257
|
...uses.filter((site) => site.shorthand === undefined).map((site) => site.node),
|
|
1143
1258
|
...links.map((link) => link.node),
|
|
1144
1259
|
...evaluations.map((evaluation) => evaluation.node),
|
|
1260
|
+
...wholes.map((whole) => whole.specifier),
|
|
1261
|
+
...urls.flatMap((url) => url.constructor ?? []),
|
|
1145
1262
|
...bindings.map((binding) => binding.node),
|
|
1146
1263
|
]),
|
|
1147
1264
|
];
|
|
@@ -1217,6 +1334,47 @@ export function references<Brand>(
|
|
|
1217
1334
|
}
|
|
1218
1335
|
}
|
|
1219
1336
|
|
|
1337
|
+
for (const url of urls) {
|
|
1338
|
+
const constructor = url.constructor === undefined ? undefined : answered.get(url.constructor);
|
|
1339
|
+
if (
|
|
1340
|
+
typeof constructor === "object" &&
|
|
1341
|
+
constructor.declarations.some((handle) => ownPaths.has(handle.path))
|
|
1342
|
+
) {
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1345
|
+
const loaded = specifierCandidates(dirnamePath(file.fileName), url.specifier)
|
|
1346
|
+
.map((path) => fileIdByName.get(path))
|
|
1347
|
+
.find((id) => id !== undefined);
|
|
1348
|
+
if (loaded !== undefined) {
|
|
1349
|
+
found.push({
|
|
1350
|
+
from: fileId,
|
|
1351
|
+
to: loaded,
|
|
1352
|
+
position: renderPosition(file, root, url.at.getStart()),
|
|
1353
|
+
use: "evaluation",
|
|
1354
|
+
resolution: "syntax",
|
|
1355
|
+
test,
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
for (const whole of wholes) {
|
|
1361
|
+
const module = answered.get(whole.specifier);
|
|
1362
|
+
if (module === undefined) {
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
const position = renderPosition(file, root, whole.at.getStart());
|
|
1366
|
+
for (const target of exportsRead(module)) {
|
|
1367
|
+
found.push({
|
|
1368
|
+
from: whole.from,
|
|
1369
|
+
to: target.id,
|
|
1370
|
+
position,
|
|
1371
|
+
use: "read",
|
|
1372
|
+
resolution: target.guessed ? "by-name" : "batch",
|
|
1373
|
+
test,
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1220
1378
|
if (markup) {
|
|
1221
1379
|
for (const symbol of declaredIn(fileId)) {
|
|
1222
1380
|
found.push({
|
package/src/roots.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { globExpression } from "./glob.ts";
|
|
|
26
26
|
import type { Host } from "./host.ts";
|
|
27
27
|
import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
|
|
28
28
|
import type { Manifest, ManifestEntry } from "./manifest.ts";
|
|
29
|
+
import { relativePath, resolvePath } from "./paths.ts";
|
|
29
30
|
import { byPosition } from "./position.ts";
|
|
30
31
|
import { UNANSWERED } from "./query.ts";
|
|
31
32
|
import type { ProjectView } from "./session.ts";
|
|
@@ -135,6 +136,50 @@ const ENTRY_KINDS = {
|
|
|
135
136
|
script: "script",
|
|
136
137
|
} as const satisfies Record<ManifestEntry["role"], RootKind>;
|
|
137
138
|
|
|
139
|
+
/** The rules whose rooted files a manifest target is read back to by name. */
|
|
140
|
+
const READ_BACK_RULES: ReadonlySet<EntryRule> = new Set([
|
|
141
|
+
"configuration-file",
|
|
142
|
+
"configuration-string",
|
|
143
|
+
"html-entry",
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
/** The extensions a manifest target's name drops: the ones a build writes. */
|
|
147
|
+
const OUTPUT_EXTENSION = /\.(?:d\.ts|d\.mts|d\.cts|js|mjs|cjs|jsx)$/u;
|
|
148
|
+
|
|
149
|
+
/** The extension a source file's name drops. */
|
|
150
|
+
const SOURCE_EXTENSION = /\.(?:d\.)?[cm]?[jt]sx?$/u;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The rooted files one manifest target reads back to by name: its path below the
|
|
154
|
+
* manifest's directory, past its first directory where it names one, without its output
|
|
155
|
+
* extension, is the name, and a rooted file is one whose path below the same directory,
|
|
156
|
+
* without its source extension, ends with that name at a directory boundary.
|
|
157
|
+
*/
|
|
158
|
+
function readBackByName(
|
|
159
|
+
host: Host,
|
|
160
|
+
entry: ManifestEntry,
|
|
161
|
+
rooted: readonly SourceFile[],
|
|
162
|
+
): readonly SourceFile[] {
|
|
163
|
+
const dir = resolvePath(host.workingDirectory(), entry.dir);
|
|
164
|
+
const below = relativePath(dir, resolvePath(host.workingDirectory(), entry.path));
|
|
165
|
+
if (below === undefined || below === "") {
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
const cut = below.indexOf("/");
|
|
169
|
+
const name = (cut === -1 ? below : below.slice(cut + 1)).replace(OUTPUT_EXTENSION, "");
|
|
170
|
+
if (name === "") {
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
const found = new Map<string, SourceFile>();
|
|
174
|
+
for (const file of rooted) {
|
|
175
|
+
const path = relativePath(dir, file.fileName)?.replace(SOURCE_EXTENSION, "");
|
|
176
|
+
if (path !== undefined && (path === name || path.endsWith(`/${name}`))) {
|
|
177
|
+
found.set(file.fileName, file);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return [...found.values()];
|
|
181
|
+
}
|
|
182
|
+
|
|
138
183
|
/** Whether a configured string carries either special character. */
|
|
139
184
|
function isPattern(text: string): boolean {
|
|
140
185
|
return text.includes("*") || text.includes("?");
|
|
@@ -245,6 +290,14 @@ export function roots<Brand>(
|
|
|
245
290
|
entered.push({ file, kind, source, exports });
|
|
246
291
|
};
|
|
247
292
|
|
|
293
|
+
const points = entryPoints(
|
|
294
|
+
project,
|
|
295
|
+
files,
|
|
296
|
+
options.testFiles,
|
|
297
|
+
options.host,
|
|
298
|
+
targetRoot,
|
|
299
|
+
options.parse,
|
|
300
|
+
);
|
|
248
301
|
const published: SourceFile[] = [];
|
|
249
302
|
const entries = [
|
|
250
303
|
...options.manifest.entries.map((entry) => ({
|
|
@@ -255,14 +308,27 @@ export function roots<Brand>(
|
|
|
255
308
|
})),
|
|
256
309
|
...(options.otherPackages ?? []).map((entry) => ({ entry, publishes: entry.published })),
|
|
257
310
|
];
|
|
311
|
+
const scripted = entries.flatMap(({ entry }) =>
|
|
312
|
+
entry.role === "script" ? files.named(entry.path) : [],
|
|
313
|
+
);
|
|
314
|
+
const readBackTo = [
|
|
315
|
+
...scripted,
|
|
316
|
+
...points.filter((point) => READ_BACK_RULES.has(point.rule)).map((point) => point.file),
|
|
317
|
+
];
|
|
318
|
+
const entryFiles = (entry: ManifestEntry): readonly SourceFile[] => {
|
|
319
|
+
const named = files.named(entry.path);
|
|
320
|
+
return named.length > 0 || entry.role === "script"
|
|
321
|
+
? named
|
|
322
|
+
: readBackByName(options.host, entry, readBackTo);
|
|
323
|
+
};
|
|
258
324
|
for (const { entry, publishes } of entries) {
|
|
259
325
|
if (options.publishedAPI && publishes) {
|
|
260
|
-
published.push(...
|
|
326
|
+
published.push(...entryFiles(entry));
|
|
261
327
|
}
|
|
262
328
|
}
|
|
263
329
|
const publishedNames = new Set(published.map((file) => file.fileName));
|
|
264
330
|
for (const { entry } of entries) {
|
|
265
|
-
for (const file of
|
|
331
|
+
for (const file of entryFiles(entry)) {
|
|
266
332
|
const kind = ENTRY_KINDS[entry.role];
|
|
267
333
|
enter(file, kind, entry.member, !publishedNames.has(file.fileName));
|
|
268
334
|
}
|
|
@@ -280,14 +346,7 @@ export function roots<Brand>(
|
|
|
280
346
|
: conventionEntries(options.conventions, files)) {
|
|
281
347
|
enter(file, "convention", row, true);
|
|
282
348
|
}
|
|
283
|
-
for (const point of
|
|
284
|
-
project,
|
|
285
|
-
files,
|
|
286
|
-
options.testFiles,
|
|
287
|
-
options.host,
|
|
288
|
-
targetRoot,
|
|
289
|
-
options.parse,
|
|
290
|
-
)) {
|
|
349
|
+
for (const point of points) {
|
|
291
350
|
enter(point.file, point.rule, point.source, point.exports);
|
|
292
351
|
}
|
|
293
352
|
for (const file of files.byPath.values()) {
|
package/src/sweep.ts
CHANGED
|
@@ -166,6 +166,25 @@ export function sweep(graph: Graph, input: SweepInput): Liveness {
|
|
|
166
166
|
return { liveUnder: live, candidates };
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Per declaration of the graph, whether the run that counts test references reaches it.
|
|
171
|
+
* That run's roots are the graph's own and each test file, being the file's top level
|
|
172
|
+
* and every declaration it exports, which a test runner may call; `held` seeds it as a
|
|
173
|
+
* mark or an exemption seeds a sweep.
|
|
174
|
+
*/
|
|
175
|
+
export function reachedWithTheTests(graph: Graph, held: readonly string[]): readonly boolean[] {
|
|
176
|
+
const tests = graph.symbols.flatMap((symbol, at) => {
|
|
177
|
+
const top =
|
|
178
|
+
symbol.kind === "file" || graph.symbols[graph.parent[at] ?? OUTSIDE]?.kind === "file";
|
|
179
|
+
return graph.test[at] === true && top && (symbol.kind === "file" || symbol.exported)
|
|
180
|
+
? [{ at, kind: "test-runner" as const }]
|
|
181
|
+
: [];
|
|
182
|
+
});
|
|
183
|
+
return reachable({ ...graph, rooted: [...graph.rooted, ...tests] }, flagged(graph, held), {
|
|
184
|
+
production: false,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
169
188
|
/**
|
|
170
189
|
* The references a loaded consumer makes that the mode counts. A production mode counts
|
|
171
190
|
* none a consumer's test file made, as it counts none of the target's.
|