@cplieger/deadset-ts 4.0.1 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/bin/component-mapper.js +5 -0
- package/dist/bin/mapper-connection.js +101 -0
- package/dist/bin/node-host.js +24 -2
- package/dist/src/analysis.js +301 -41
- package/dist/src/component-files.js +612 -0
- package/dist/src/component-layer.js +132 -0
- package/dist/src/config.js +9 -2
- package/dist/src/configuration-files.js +290 -0
- package/dist/src/conformance-record.js +3 -9
- package/dist/src/contextual-uses.js +240 -0
- package/dist/src/convention-rows.js +389 -0
- package/dist/src/conventions.js +565 -0
- package/dist/src/dependencies.js +36 -3
- package/dist/src/destructuring.js +6 -6
- package/dist/src/discover.js +10 -3
- package/dist/src/emit-map.js +72 -0
- package/dist/src/entry-point-gaps.js +81 -35
- package/dist/src/entry-points.js +50 -32
- package/dist/src/exempt-classes.js +6 -1
- package/dist/src/exempt.js +32 -20
- package/dist/src/findings/completion.js +46 -32
- package/dist/src/findings/emitters.js +22 -1
- 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 +47 -3
- package/dist/src/generated-file.js +24 -0
- package/dist/src/graph.js +13 -5
- package/dist/src/interface-satisfaction.js +2 -68
- package/dist/src/inventory.js +8 -3
- package/dist/src/kinds.js +6 -5
- package/dist/src/manifest.js +37 -11
- package/dist/src/matrix.js +21 -13
- package/dist/src/partial.js +3 -1
- package/dist/src/pnpm-workspace.js +120 -0
- package/dist/src/position.js +17 -2
- package/dist/src/print.js +2 -0
- package/dist/src/query.js +15 -0
- package/dist/src/references.js +404 -36
- package/dist/src/report.js +26 -6
- package/dist/src/reporters/annotations.js +10 -3
- package/dist/src/reporters/template.js +3 -0
- package/dist/src/resolve.js +37 -11
- package/dist/src/roots.js +37 -8
- package/dist/src/run.js +13 -3
- package/dist/src/schema.js +2 -0
- package/dist/src/session.js +141 -34
- package/dist/src/setup-failure.js +23 -0
- package/dist/src/source-files.js +4 -61
- package/dist/src/stores.js +3 -0
- package/dist/src/sweep.js +4 -2
- package/dist/src/template-field.js +38 -6
- package/dist/src/test-support.js +59 -0
- package/dist/src/type-errors.js +252 -0
- package/dist/src/type-query-alias.js +92 -0
- package/dist/src/value-flow.js +74 -0
- package/dist/src/verbs/analyze.js +24 -4
- package/dist/src/verbs/explain.js +4 -1
- package/dist/src/verbs/print-projects.js +17 -56
- package/dist/src/verbs/print-retained.js +7 -2
- package/dist/src/verbs/print-roots.js +7 -2
- package/dist/src/version-range.js +255 -0
- package/dist/src/version.js +1 -1
- package/dist/src/workspace-manifests.js +68 -0
- package/dist/src/workspace-programs.js +342 -0
- package/dist/src/workspace-resolution.js +307 -0
- package/dist/src/workspace-run.js +75 -0
- package/dist/src/workspace.js +358 -0
- package/jsr.json +1 -1
- package/package.json +3 -2
- package/src/analysis.ts +475 -72
- package/src/component-files.ts +718 -0
- package/src/component-layer.ts +160 -0
- package/src/config.ts +11 -2
- package/src/configuration-files.ts +370 -0
- package/src/conformance-record.ts +3 -10
- package/src/contextual-uses.ts +354 -0
- package/src/convention-rows.ts +448 -0
- package/src/conventions.ts +788 -0
- package/src/dependencies.ts +49 -2
- package/src/destructuring.ts +16 -6
- package/src/discover.ts +10 -6
- package/src/emit-map.ts +93 -0
- package/src/entry-point-gaps.ts +92 -38
- package/src/entry-points.ts +79 -36
- package/src/exempt-classes.ts +6 -1
- package/src/exempt.ts +69 -20
- package/src/file-facts.ts +2 -0
- package/src/findings/completion.ts +55 -36
- package/src/findings/emitters.ts +32 -1
- package/src/findings/interfaces.ts +9 -2
- package/src/findings/reads-and-writes.ts +70 -9
- package/src/findings/unused-declarations.ts +60 -3
- package/src/generated-file.ts +27 -0
- package/src/graph.ts +15 -4
- package/src/host.ts +23 -0
- package/src/index.ts +1 -1
- package/src/interface-satisfaction.ts +1 -92
- package/src/inventory.ts +17 -3
- package/src/kinds.ts +7 -5
- package/src/manifest.ts +47 -14
- package/src/matrix.ts +43 -15
- package/src/partial.ts +6 -1
- package/src/pnpm-workspace.ts +135 -0
- package/src/position.ts +23 -2
- package/src/print.ts +2 -0
- package/src/query.ts +26 -0
- package/src/references.ts +472 -40
- package/src/report.ts +56 -6
- package/src/reporters/annotations.ts +12 -3
- package/src/reporters/template.ts +3 -0
- package/src/resolve.ts +65 -11
- package/src/roots.ts +70 -9
- package/src/run.ts +15 -5
- package/src/schema.ts +2 -0
- package/src/session.ts +218 -37
- package/src/setup-failure.ts +39 -0
- package/src/source-files.ts +4 -66
- package/src/stores.ts +9 -0
- package/src/sweep.ts +9 -2
- package/src/template-field.ts +42 -7
- package/src/test-support.ts +77 -0
- package/src/type-errors.ts +340 -0
- package/src/type-query-alias.ts +133 -0
- package/src/value-flow.ts +104 -0
- package/src/verbs/analyze.ts +38 -4
- package/src/verbs/explain.ts +4 -1
- package/src/verbs/print-projects.ts +17 -69
- package/src/verbs/print-retained.ts +7 -2
- package/src/verbs/print-roots.ts +7 -2
- package/src/verbs/verb.ts +6 -3
- package/src/version-range.ts +309 -0
- package/src/version.ts +1 -1
- package/src/workspace-manifests.ts +100 -0
- package/src/workspace-programs.ts +486 -0
- package/src/workspace-resolution.ts +393 -0
- package/src/workspace-run.ts +148 -0
- package/src/workspace.ts +463 -0
package/src/references.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isArrayLiteralExpression,
|
|
3
|
+
isAsExpression,
|
|
3
4
|
isBinaryExpression,
|
|
4
5
|
isCallExpression,
|
|
5
6
|
isDecorator,
|
|
6
7
|
isDeleteExpression,
|
|
7
8
|
isElementAccessExpression,
|
|
8
9
|
isExportDeclaration,
|
|
10
|
+
isExpressionStatement,
|
|
9
11
|
isExternalModuleReference,
|
|
10
12
|
isForInStatement,
|
|
11
13
|
isForOfStatement,
|
|
14
|
+
isForStatement,
|
|
12
15
|
isImportDeclaration,
|
|
13
16
|
isImportEqualsDeclaration,
|
|
14
17
|
isNamespaceExport,
|
|
@@ -23,19 +26,23 @@ import {
|
|
|
23
26
|
isSpreadAssignment,
|
|
24
27
|
isSpreadElement,
|
|
25
28
|
isStringLiteral,
|
|
29
|
+
isVoidExpression,
|
|
26
30
|
SyntaxKind,
|
|
27
31
|
type Node,
|
|
28
32
|
type SourceFile,
|
|
29
33
|
} from "@typescript/native/unstable/ast";
|
|
30
34
|
import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
|
|
31
35
|
import { aliasChains, type ChainTarget } from "./alias-chain.ts";
|
|
36
|
+
import { contextualUses, type FlowAt, type LiteralAt } from "./contextual-uses.ts";
|
|
32
37
|
import { destructuring, propertyReadsOf, type PropertyRead } from "./destructuring.ts";
|
|
33
38
|
import { globExpression } from "./glob.ts";
|
|
34
|
-
import { declarationsByName, nodeKey, type Inventory } from "./inventory.ts";
|
|
39
|
+
import { declarationsByName, nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
|
|
35
40
|
import { relativePath } from "./paths.ts";
|
|
36
|
-
import { byPosition, renderPosition, type Position } from "./position.ts";
|
|
37
|
-
import { DEFAULT_BATCH_CAP, UNANSWERED, type Answer } from "./query.ts";
|
|
41
|
+
import { byPosition, isComponentFile, renderPosition, type Position } from "./position.ts";
|
|
42
|
+
import { DEFAULT_BATCH_CAP, propertyTables, UNANSWERED, type Answer } from "./query.ts";
|
|
38
43
|
import type { ProjectView } from "./session.ts";
|
|
44
|
+
import { typeQueryAliases } from "./type-query-alias.ts";
|
|
45
|
+
import { handedOn, valuesOf } from "./value-flow.ts";
|
|
39
46
|
|
|
40
47
|
/**
|
|
41
48
|
* Every use one project's own files make of the project's own declarations.
|
|
@@ -64,15 +71,22 @@ import type { ProjectView } from "./session.ts";
|
|
|
64
71
|
export type Use = "read" | "write" | "evaluation" | "decorator";
|
|
65
72
|
|
|
66
73
|
/**
|
|
67
|
-
* Which path answered for one reference: `batch`, the batched lookup over a file's
|
|
68
|
-
*
|
|
69
|
-
* `
|
|
70
|
-
*
|
|
71
|
-
* `
|
|
72
|
-
* `by-name`, every declaration
|
|
74
|
+
* Which path answered for one reference: `batch`, the batched lookup over a file's names;
|
|
75
|
+
* `resolved-symbol`, the per-node lookup for a batch's residue; `shorthand`, the value
|
|
76
|
+
* `{ a }` reads; `alias`, a step along an import or export chain; `syntax`, the tree alone
|
|
77
|
+
* (a decorator's target, a component's markup); `destructured`, the property a pattern
|
|
78
|
+
* names; `contextual`, the member a literal property writes or a value's position reads;
|
|
79
|
+
* `by-name`, every declaration spelled as the site is, where the checker failed.
|
|
73
80
|
*/
|
|
74
81
|
export type Resolution =
|
|
75
|
-
|
|
82
|
+
| "batch"
|
|
83
|
+
| "resolved-symbol"
|
|
84
|
+
| "shorthand"
|
|
85
|
+
| "alias"
|
|
86
|
+
| "syntax"
|
|
87
|
+
| "destructured"
|
|
88
|
+
| "contextual"
|
|
89
|
+
| "by-name";
|
|
76
90
|
|
|
77
91
|
/** One use of one declaration by one declaration. */
|
|
78
92
|
export interface Reference {
|
|
@@ -98,6 +112,12 @@ export interface Reference {
|
|
|
98
112
|
* the consumer's root.
|
|
99
113
|
*/
|
|
100
114
|
readonly consumer?: string;
|
|
115
|
+
/**
|
|
116
|
+
* On a write, the local names of the writing file's import bindings that only the
|
|
117
|
+
* values its file writes into the same declaration use, which deleting the
|
|
118
|
+
* declaration with its writes deletes too.
|
|
119
|
+
*/
|
|
120
|
+
readonly alone?: readonly string[];
|
|
101
121
|
}
|
|
102
122
|
|
|
103
123
|
/** The consumer one reference pass reads the files of, beside the target it names. */
|
|
@@ -144,6 +164,13 @@ export interface ReferenceCost {
|
|
|
144
164
|
* pattern destructures, and each step a nested assignment target takes into its value.
|
|
145
165
|
*/
|
|
146
166
|
readonly patternLookups: number;
|
|
167
|
+
/**
|
|
168
|
+
* Batched lookups for the contextual uses: the contextual types of the object literals
|
|
169
|
+
* and of the values that carry a member, and the values' types, each per capped run.
|
|
170
|
+
*/
|
|
171
|
+
readonly contextualBatches: number;
|
|
172
|
+
/** Per-type lookups for the contextual uses: each distinct type's constituents, properties and symbol. */
|
|
173
|
+
readonly contextualLookups: number;
|
|
147
174
|
}
|
|
148
175
|
|
|
149
176
|
/** One project's references, in position order, and what reading them cost. */
|
|
@@ -184,9 +211,8 @@ export interface ReferenceOptions {
|
|
|
184
211
|
|
|
185
212
|
/**
|
|
186
213
|
* The operators whose left side one assignment stores into. A compound assignment is
|
|
187
|
-
* among them: it reads its target only to write the result
|
|
188
|
-
*
|
|
189
|
-
* carries no information out of itself.
|
|
214
|
+
* among them: where its value is discarded it reads its target only to write the result
|
|
215
|
+
* back, so nothing else receives what it read.
|
|
190
216
|
*/
|
|
191
217
|
const ASSIGNMENTS: ReadonlySet<SyntaxKind> = new Set([
|
|
192
218
|
SyntaxKind.EqualsToken,
|
|
@@ -207,6 +233,40 @@ const ASSIGNMENTS: ReadonlySet<SyntaxKind> = new Set([
|
|
|
207
233
|
SyntaxKind.CaretEqualsToken,
|
|
208
234
|
]);
|
|
209
235
|
|
|
236
|
+
/**
|
|
237
|
+
* The assignments that read their target to decide whether to store at all, so the read
|
|
238
|
+
* carries information out whatever becomes of the result.
|
|
239
|
+
*/
|
|
240
|
+
const LOGICAL_ASSIGNMENTS: ReadonlySet<SyntaxKind> = new Set([
|
|
241
|
+
SyntaxKind.BarBarEqualsToken,
|
|
242
|
+
SyntaxKind.AmpersandAmpersandEqualsToken,
|
|
243
|
+
SyntaxKind.QuestionQuestionEqualsToken,
|
|
244
|
+
]);
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Whether the value of one store expression is discarded: it is a statement of its own, a
|
|
248
|
+
* clause of a `for` loop, the operand of `void`, or the left operand of a comma.
|
|
249
|
+
*/
|
|
250
|
+
function valueDiscarded(node: Node): boolean {
|
|
251
|
+
let child = node;
|
|
252
|
+
let parent = node.parent;
|
|
253
|
+
while (isParenthesizedExpression(parent)) {
|
|
254
|
+
child = parent;
|
|
255
|
+
parent = parent.parent;
|
|
256
|
+
}
|
|
257
|
+
if (isExpressionStatement(parent) || isVoidExpression(parent)) {
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
if (isForStatement(parent)) {
|
|
261
|
+
return parent.initializer === child || parent.incrementor === child;
|
|
262
|
+
}
|
|
263
|
+
return (
|
|
264
|
+
isBinaryExpression(parent) &&
|
|
265
|
+
parent.operatorToken.kind === SyntaxKind.CommaToken &&
|
|
266
|
+
parent.left === child
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
210
270
|
/** Whether one node is an identifier or a private name, the two forms a name takes. */
|
|
211
271
|
function isName(node: Node): boolean {
|
|
212
272
|
return node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.PrivateIdentifier;
|
|
@@ -243,7 +303,11 @@ function markWrite(target: Node, writes: Set<number>): void {
|
|
|
243
303
|
return;
|
|
244
304
|
}
|
|
245
305
|
if (isElementAccessExpression(held)) {
|
|
246
|
-
|
|
306
|
+
// A store through a member's value lands in storage every holder of the value
|
|
307
|
+
// reads, so it reads the member; a variable only ever stored into stays written.
|
|
308
|
+
if (!isPropertyAccessExpression(unparenthesized(held.expression))) {
|
|
309
|
+
markWrite(held.expression, writes);
|
|
310
|
+
}
|
|
247
311
|
return;
|
|
248
312
|
}
|
|
249
313
|
if (isArrayLiteralExpression(held)) {
|
|
@@ -277,12 +341,20 @@ function markWrite(target: Node, writes: Set<number>): void {
|
|
|
277
341
|
* Records every store one node performs: an assignment, a compound assignment, an
|
|
278
342
|
* increment or a decrement, a `delete`, and an iteration that assigns to a name declared
|
|
279
343
|
* elsewhere. An iteration that declares its own name stores into nothing the inventory
|
|
280
|
-
* holds.
|
|
344
|
+
* holds. A store that reads its target as well records it in `reads`: a logical
|
|
345
|
+
* assignment, and a compound assignment, an increment or a decrement whose value is used.
|
|
281
346
|
*/
|
|
282
|
-
function markStores(node: Node, writes: Set<number>): void {
|
|
347
|
+
function markStores(node: Node, writes: Set<number>, reads: Set<number>): void {
|
|
283
348
|
if (isBinaryExpression(node)) {
|
|
284
|
-
|
|
349
|
+
const operator = node.operatorToken.kind;
|
|
350
|
+
if (ASSIGNMENTS.has(operator)) {
|
|
285
351
|
markWrite(node.left, writes);
|
|
352
|
+
if (
|
|
353
|
+
LOGICAL_ASSIGNMENTS.has(operator) ||
|
|
354
|
+
(operator !== SyntaxKind.EqualsToken && !valueDiscarded(node))
|
|
355
|
+
) {
|
|
356
|
+
markWrite(node.left, reads);
|
|
357
|
+
}
|
|
286
358
|
}
|
|
287
359
|
return;
|
|
288
360
|
}
|
|
@@ -292,6 +364,9 @@ function markStores(node: Node, writes: Set<number>): void {
|
|
|
292
364
|
node.operator === SyntaxKind.MinusMinusToken
|
|
293
365
|
) {
|
|
294
366
|
markWrite(node.operand, writes);
|
|
367
|
+
if (!valueDiscarded(node)) {
|
|
368
|
+
markWrite(node.operand, reads);
|
|
369
|
+
}
|
|
295
370
|
}
|
|
296
371
|
return;
|
|
297
372
|
}
|
|
@@ -359,6 +434,8 @@ interface Read extends PropertyRead {
|
|
|
359
434
|
/** Everything one file's walk found to resolve, and the decorators it needs no resolution for. */
|
|
360
435
|
interface FileSites {
|
|
361
436
|
readonly uses: readonly Site[];
|
|
437
|
+
readonly literals: readonly LiteralAt[];
|
|
438
|
+
readonly flows: readonly FlowAt[];
|
|
362
439
|
readonly reads: readonly Read[];
|
|
363
440
|
readonly links: readonly Link[];
|
|
364
441
|
readonly evaluations: readonly Evaluation[];
|
|
@@ -443,24 +520,53 @@ function linksOf(
|
|
|
443
520
|
return found;
|
|
444
521
|
}
|
|
445
522
|
|
|
523
|
+
/**
|
|
524
|
+
* Whether one object literal is a destructuring target, written on the left of an
|
|
525
|
+
* assignment or as a `for` loop's initializer, through the literals and spreads it nests in.
|
|
526
|
+
*/
|
|
527
|
+
function isAssignmentPattern(node: Node): boolean {
|
|
528
|
+
let child = node;
|
|
529
|
+
let parent = node.parent;
|
|
530
|
+
while (
|
|
531
|
+
isParenthesizedExpression(parent) ||
|
|
532
|
+
isArrayLiteralExpression(parent) ||
|
|
533
|
+
isObjectLiteralExpression(parent) ||
|
|
534
|
+
isSpreadElement(parent) ||
|
|
535
|
+
isSpreadAssignment(parent) ||
|
|
536
|
+
(isPropertyAssignment(parent) && parent.initializer === child)
|
|
537
|
+
) {
|
|
538
|
+
child = parent;
|
|
539
|
+
parent = parent.parent;
|
|
540
|
+
}
|
|
541
|
+
if (isBinaryExpression(parent)) {
|
|
542
|
+
return parent.operatorToken.kind === SyntaxKind.EqualsToken && parent.left === child;
|
|
543
|
+
}
|
|
544
|
+
return (isForInStatement(parent) || isForOfStatement(parent)) && parent.initializer === child;
|
|
545
|
+
}
|
|
546
|
+
|
|
446
547
|
/**
|
|
447
548
|
* Every name node one file holds that is a use rather than a declaration, in source
|
|
448
549
|
* order, each with its enclosing declaration and its use, and every re-export the file
|
|
449
550
|
* writes. One walk answers all of it, because a second would have to agree with this one
|
|
450
551
|
* about which nodes are names. A declaration's own declared name is left out, which is
|
|
451
|
-
* what makes a declaration referenced only from its own site an unreferenced one
|
|
552
|
+
* what makes a declaration referenced only from its own site an unreferenced one, and so
|
|
553
|
+
* is every node of `unread`, a subtree that is no use.
|
|
452
554
|
*/
|
|
453
555
|
function sitesOf(
|
|
454
556
|
file: SourceFile,
|
|
455
557
|
declarations: ReadonlyMap<string, string>,
|
|
456
558
|
fileId: string,
|
|
559
|
+
unread: ReadonlySet<Node>,
|
|
457
560
|
): FileSites {
|
|
458
561
|
const found: Site[] = [];
|
|
459
562
|
const reads: Read[] = [];
|
|
460
563
|
const links: Link[] = [];
|
|
461
564
|
const evaluations: Evaluation[] = [];
|
|
462
565
|
const decorated: Decorated[] = [];
|
|
566
|
+
const literals: LiteralAt[] = [];
|
|
567
|
+
const flows: FlowAt[] = [];
|
|
463
568
|
const writes = new Set<number>();
|
|
569
|
+
const alsoRead = new Set<number>();
|
|
464
570
|
const declared = new Set<number>();
|
|
465
571
|
const shorthands = new Map<number, Node>();
|
|
466
572
|
let enclosing = fileId;
|
|
@@ -484,19 +590,30 @@ function sitesOf(
|
|
|
484
590
|
};
|
|
485
591
|
|
|
486
592
|
const visit = (node: Node): void => {
|
|
593
|
+
if (unread.has(node)) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
487
596
|
if (isName(node)) {
|
|
488
597
|
if (!declared.has(node.pos)) {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
598
|
+
const site = { node, shorthand: shorthands.get(node.pos), from: enclosing };
|
|
599
|
+
if (writes.has(node.pos)) {
|
|
600
|
+
found.push({ ...site, use: "write" });
|
|
601
|
+
}
|
|
602
|
+
if (!writes.has(node.pos) || alsoRead.has(node.pos)) {
|
|
603
|
+
found.push({ ...site, use: "read" });
|
|
604
|
+
}
|
|
495
605
|
}
|
|
496
606
|
return;
|
|
497
607
|
}
|
|
498
608
|
markNames(node);
|
|
499
|
-
markStores(node, writes);
|
|
609
|
+
markStores(node, writes, alsoRead);
|
|
610
|
+
if (isObjectLiteralExpression(node) && !isAssignmentPattern(node)) {
|
|
611
|
+
literals.push({ literal: node, from: enclosing });
|
|
612
|
+
}
|
|
613
|
+
// An assertion needs only comparability, so no member of its operand is read by it.
|
|
614
|
+
for (const written of isAsExpression(node) ? [] : handedOn(node)) {
|
|
615
|
+
flows.push(...valuesOf(written).map((value) => ({ value, from: enclosing })));
|
|
616
|
+
}
|
|
500
617
|
for (const read of propertyReadsOf(node)) {
|
|
501
618
|
reads.push({ ...read, from: enclosing });
|
|
502
619
|
}
|
|
@@ -520,9 +637,21 @@ function sitesOf(
|
|
|
520
637
|
};
|
|
521
638
|
|
|
522
639
|
file.forEachChild(visit);
|
|
523
|
-
return { uses: found, reads, links, evaluations, decorated };
|
|
640
|
+
return { uses: found, literals, flows, reads, links, evaluations, decorated };
|
|
524
641
|
}
|
|
525
642
|
|
|
643
|
+
/** The declaration kinds of a property an object literal declares. */
|
|
644
|
+
const LITERAL_PROPERTY_KINDS: ReadonlySet<SyntaxKind> = new Set([
|
|
645
|
+
SyntaxKind.PropertyAssignment,
|
|
646
|
+
SyntaxKind.ShorthandPropertyAssignment,
|
|
647
|
+
SyntaxKind.MethodDeclaration,
|
|
648
|
+
SyntaxKind.GetAccessor,
|
|
649
|
+
SyntaxKind.SetAccessor,
|
|
650
|
+
]);
|
|
651
|
+
|
|
652
|
+
/** The rule a file the analysis classified as test-support code is counted under. */
|
|
653
|
+
const TEST_SUPPORT_RULE = "test-support";
|
|
654
|
+
|
|
526
655
|
/**
|
|
527
656
|
* The name one test-file rule takes. A rule's name is a word list the report's own
|
|
528
657
|
* vocabulary spells, which cannot carry a pattern, so a rule is named by the place of
|
|
@@ -539,17 +668,20 @@ function patternRule(index: number): string {
|
|
|
539
668
|
export function testFileRulesOf(
|
|
540
669
|
testFiles: readonly string[],
|
|
541
670
|
paths: readonly string[],
|
|
671
|
+
supportFiles: readonly string[] = [],
|
|
542
672
|
): readonly TestFileRule[] {
|
|
543
673
|
const distinct = [...new Set(paths)];
|
|
544
|
-
|
|
545
|
-
|
|
674
|
+
const support = new Set(supportFiles).size;
|
|
675
|
+
return [
|
|
676
|
+
...testFiles.map((pattern, index) => {
|
|
546
677
|
const expression = globExpression(pattern);
|
|
547
678
|
return {
|
|
548
679
|
rule: patternRule(index),
|
|
549
680
|
matched: distinct.filter((path) => expression.test(path)).length,
|
|
550
681
|
};
|
|
551
|
-
})
|
|
552
|
-
|
|
682
|
+
}),
|
|
683
|
+
...(support === 0 ? [] : [{ rule: TEST_SUPPORT_RULE, matched: support }]),
|
|
684
|
+
].sort((a, b) => compare(a.rule, b.rule));
|
|
553
685
|
}
|
|
554
686
|
|
|
555
687
|
const importedCache = new WeakMap<
|
|
@@ -599,6 +731,101 @@ function importedNames(file: SourceFile): ReadonlyMap<string, readonly (string |
|
|
|
599
731
|
return names;
|
|
600
732
|
}
|
|
601
733
|
|
|
734
|
+
/**
|
|
735
|
+
* The name node of every import binding one file's top level declares, and whether it
|
|
736
|
+
* binds a whole module.
|
|
737
|
+
*/
|
|
738
|
+
function importBindings(file: SourceFile): readonly { node: Node; whole: boolean }[] {
|
|
739
|
+
const found: { node: Node; whole: boolean }[] = [];
|
|
740
|
+
for (const statement of file.statements) {
|
|
741
|
+
if (isImportEqualsDeclaration(statement)) {
|
|
742
|
+
found.push({ node: statement.name, whole: true });
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
const clause = isImportDeclaration(statement) ? statement.importClause : undefined;
|
|
746
|
+
if (clause === undefined) {
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
if (clause.name !== undefined) {
|
|
750
|
+
found.push({ node: clause.name, whole: false });
|
|
751
|
+
}
|
|
752
|
+
const bindings = clause.namedBindings;
|
|
753
|
+
if (bindings === undefined) {
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
if (bindings.kind === SyntaxKind.NamespaceImport) {
|
|
757
|
+
found.push({ node: bindings.name, whole: true });
|
|
758
|
+
} else {
|
|
759
|
+
found.push(...bindings.elements.map((element) => ({ node: element.name, whole: false })));
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return found;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/** The value one assignment stores through the name it writes, where it is a plain assignment. */
|
|
766
|
+
function storedValue(name: Node): Node | undefined {
|
|
767
|
+
let target: Node = name;
|
|
768
|
+
if (isPropertyAccessExpression(name.parent) && name.parent.name === name) {
|
|
769
|
+
target = name.parent;
|
|
770
|
+
}
|
|
771
|
+
while (isParenthesizedExpression(target.parent)) {
|
|
772
|
+
target = target.parent;
|
|
773
|
+
}
|
|
774
|
+
const parent = target.parent;
|
|
775
|
+
return isBinaryExpression(parent) &&
|
|
776
|
+
parent.operatorToken.kind === SyntaxKind.EqualsToken &&
|
|
777
|
+
parent.left === target
|
|
778
|
+
? parent.right
|
|
779
|
+
: undefined;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* The import bindings of one file that only the values written into one declaration
|
|
784
|
+
* use, for each write into it. A binding is named by an identifier that spells its
|
|
785
|
+
* local name and is not the member a property access names.
|
|
786
|
+
*/
|
|
787
|
+
function aloneIn(
|
|
788
|
+
file: SourceFile,
|
|
789
|
+
uses: readonly Site[],
|
|
790
|
+
writes: readonly { index: number; value: Node }[],
|
|
791
|
+
found: readonly Reference[],
|
|
792
|
+
): readonly { index: number; names: readonly string[] }[] {
|
|
793
|
+
const bindings = new Set(importBindings(file).map((binding) => binding.node.getText()));
|
|
794
|
+
if (bindings.size === 0) {
|
|
795
|
+
return [];
|
|
796
|
+
}
|
|
797
|
+
const occurrences = new Map<string, Node[]>();
|
|
798
|
+
for (const site of uses) {
|
|
799
|
+
const text = site.node.getText();
|
|
800
|
+
const parent = site.node.parent;
|
|
801
|
+
if (!bindings.has(text) || (isPropertyAccessExpression(parent) && parent.name === site.node)) {
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
if (!occurrences.get(text)?.includes(site.node)) {
|
|
805
|
+
occurrences.set(text, [...(occurrences.get(text) ?? []), site.node]);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
const inside = (node: Node, value: Node): boolean =>
|
|
809
|
+
node.pos >= value.pos && node.end <= value.end;
|
|
810
|
+
const result: { index: number; names: readonly string[] }[] = [];
|
|
811
|
+
for (const write of writes) {
|
|
812
|
+
const to = found[write.index]?.to;
|
|
813
|
+
const sameTarget = writes.filter((one) => found[one.index]?.to === to);
|
|
814
|
+
const names = [...occurrences]
|
|
815
|
+
.filter(
|
|
816
|
+
([, nodes]) =>
|
|
817
|
+
nodes.some((node) => inside(node, write.value)) &&
|
|
818
|
+
nodes.every((node) => sameTarget.some((one) => inside(node, one.value))),
|
|
819
|
+
)
|
|
820
|
+
.map(([name]) => name)
|
|
821
|
+
.sort(compare);
|
|
822
|
+
if (names.length > 0) {
|
|
823
|
+
result.push({ index: write.index, names });
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return result;
|
|
827
|
+
}
|
|
828
|
+
|
|
602
829
|
/** Two strings ordered bytewise, which is the order every set of a run is read in. */
|
|
603
830
|
function compare(a: string, b: string): number {
|
|
604
831
|
if (a === b) {
|
|
@@ -635,7 +862,8 @@ export function references<Brand>(
|
|
|
635
862
|
const found: Reference[] = [];
|
|
636
863
|
const tests: string[] = [];
|
|
637
864
|
const chains = aliasChains(project, held);
|
|
638
|
-
const
|
|
865
|
+
const tables = propertyTables(project.queries);
|
|
866
|
+
const destructured = destructuring(project, cap, tables);
|
|
639
867
|
let batched = 0;
|
|
640
868
|
let fileBatches = 0;
|
|
641
869
|
let residueFallbacks = 0;
|
|
@@ -653,9 +881,24 @@ export function references<Brand>(
|
|
|
653
881
|
relativePath(consumer.root, file.fileName) !== undefined &&
|
|
654
882
|
relativePath(targetRoot, file.fileName) === undefined,
|
|
655
883
|
);
|
|
884
|
+
const ownPaths = project.ownPaths();
|
|
656
885
|
const fileIds = new Set(
|
|
657
886
|
held.symbols.filter((symbol) => symbol.kind === "file").map((symbol) => symbol.id),
|
|
658
887
|
);
|
|
888
|
+
let topLevel: ReadonlyMap<string, readonly InventorySymbol[]> | undefined;
|
|
889
|
+
/** The declarations one file's top level holds. */
|
|
890
|
+
const declaredIn = (fileId: string): readonly InventorySymbol[] => {
|
|
891
|
+
if (topLevel === undefined) {
|
|
892
|
+
const byParent = new Map<string, InventorySymbol[]>();
|
|
893
|
+
for (const symbol of held.symbols) {
|
|
894
|
+
if (fileIds.has(symbol.parent)) {
|
|
895
|
+
byParent.set(symbol.parent, [...(byParent.get(symbol.parent) ?? []), symbol]);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
topLevel = byParent;
|
|
899
|
+
}
|
|
900
|
+
return topLevel.get(fileId) ?? [];
|
|
901
|
+
};
|
|
659
902
|
|
|
660
903
|
/** The identifier of the declaration one node of this program is, where it is one. */
|
|
661
904
|
const declaredAt = (node: Node): string | undefined =>
|
|
@@ -693,6 +936,49 @@ export function references<Brand>(
|
|
|
693
936
|
];
|
|
694
937
|
};
|
|
695
938
|
|
|
939
|
+
// A type-query alias's own type names its export without using it; a use of the
|
|
940
|
+
// global is the use, so it names every link of the export's chain as well.
|
|
941
|
+
const aliases = project.ownSourceFiles().flatMap(typeQueryAliases);
|
|
942
|
+
const unread = new Set(aliases.map((alias) => alias.type));
|
|
943
|
+
const aliased = new Map<string, readonly ChainTarget[]>();
|
|
944
|
+
if (aliases.length > 0) {
|
|
945
|
+
const exported = project.symbolsAt(aliases.map((alias) => project.handle(alias.exported)));
|
|
946
|
+
aliases.forEach((alias, index) => {
|
|
947
|
+
const id = declaredAt(alias.declaration);
|
|
948
|
+
const symbol = exported[index];
|
|
949
|
+
if (id === undefined || symbol === undefined) {
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
aliased.set(
|
|
953
|
+
id,
|
|
954
|
+
symbol === UNANSWERED
|
|
955
|
+
? (declarationsByName(held).get(alias.name) ?? []).map((one) => ({
|
|
956
|
+
id: one,
|
|
957
|
+
stepped: true,
|
|
958
|
+
guessed: true,
|
|
959
|
+
}))
|
|
960
|
+
: chains.chainOf(symbol).map((target) => ({ ...target, stepped: true })),
|
|
961
|
+
);
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
/** One use's targets, then every link an alias global among them stands for, each once. */
|
|
965
|
+
const throughAliases = (targets: readonly ChainTarget[]): readonly ChainTarget[] => {
|
|
966
|
+
if (aliased.size === 0) {
|
|
967
|
+
return targets;
|
|
968
|
+
}
|
|
969
|
+
const all = [...targets];
|
|
970
|
+
const seen = new Set(targets.map((target) => target.id));
|
|
971
|
+
for (const target of targets) {
|
|
972
|
+
for (const forwarded of aliased.get(target.id) ?? []) {
|
|
973
|
+
if (!seen.has(forwarded.id)) {
|
|
974
|
+
seen.add(forwarded.id);
|
|
975
|
+
all.push(forwarded);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
return all;
|
|
980
|
+
};
|
|
981
|
+
|
|
696
982
|
/** The symbol one site resolves to, asking only the accessor the site's form names. */
|
|
697
983
|
const symbolFor = (
|
|
698
984
|
site: Site,
|
|
@@ -710,17 +996,102 @@ export function references<Brand>(
|
|
|
710
996
|
return project.resolvedSymbolAt(project.handle(site.node));
|
|
711
997
|
};
|
|
712
998
|
|
|
713
|
-
|
|
999
|
+
const walked = files.map((file) => {
|
|
1000
|
+
const fileId = declaredAt(file) ?? "";
|
|
1001
|
+
return { file, fileId, ...sitesOf(file, declarations, fileId, unread) };
|
|
1002
|
+
});
|
|
1003
|
+
const contextual = contextualUses(
|
|
1004
|
+
project,
|
|
1005
|
+
held,
|
|
1006
|
+
chains,
|
|
1007
|
+
walked.flatMap((one) => one.literals),
|
|
1008
|
+
walked.flatMap((one) => one.flows),
|
|
1009
|
+
cap,
|
|
1010
|
+
tables,
|
|
1011
|
+
);
|
|
1012
|
+
const writtenBy = new Map<string, readonly string[]>();
|
|
1013
|
+
for (const write of contextual.writes) {
|
|
1014
|
+
const property = write.at.parent;
|
|
1015
|
+
writtenBy.set(nodeKey(property.getSourceFile(), property), write.ids);
|
|
1016
|
+
}
|
|
1017
|
+
const forwardedCache = new Map<number, readonly ChainTarget[]>();
|
|
1018
|
+
/**
|
|
1019
|
+
* The members a literal property one symbol declares writes: a use of the property
|
|
1020
|
+
* reaches them, because the literal's type carries the data the member describes.
|
|
1021
|
+
*/
|
|
1022
|
+
const forwarded = (symbol: TSSymbol): readonly ChainTarget[] => {
|
|
1023
|
+
const known = forwardedCache.get(symbol.id);
|
|
1024
|
+
if (known !== undefined) {
|
|
1025
|
+
return known;
|
|
1026
|
+
}
|
|
1027
|
+
const found: ChainTarget[] = [];
|
|
1028
|
+
if (writtenBy.size > 0) {
|
|
1029
|
+
for (const handle of symbol.declarations) {
|
|
1030
|
+
if (!LITERAL_PROPERTY_KINDS.has(handle.kind) || !ownPaths.has(handle.path)) {
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
const node = project.declarationAt(handle)?.node;
|
|
1034
|
+
const ids =
|
|
1035
|
+
node === undefined ? undefined : writtenBy.get(nodeKey(node.getSourceFile(), node));
|
|
1036
|
+
for (const id of ids ?? []) {
|
|
1037
|
+
if (!found.some((target) => target.id === id)) {
|
|
1038
|
+
found.push({ id, stepped: true, guessed: false });
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
forwardedCache.set(symbol.id, found);
|
|
1044
|
+
return found;
|
|
1045
|
+
};
|
|
1046
|
+
/** One resolved symbol's declarations along its chain, and the members its literal properties write. */
|
|
1047
|
+
const targetsOf = (symbol: TSSymbol): readonly ChainTarget[] => {
|
|
1048
|
+
const through = forwarded(symbol);
|
|
1049
|
+
return through.length === 0 ? chains.chainOf(symbol) : [...chains.chainOf(symbol), ...through];
|
|
1050
|
+
};
|
|
1051
|
+
const testFile = new Map<SourceFile, boolean>();
|
|
1052
|
+
for (const { file } of walked) {
|
|
714
1053
|
const path = renderPosition(file, root, 0).path;
|
|
715
1054
|
const test = patterns.some((pattern) => pattern.test(path));
|
|
1055
|
+
testFile.set(file, test);
|
|
716
1056
|
if (test) {
|
|
717
1057
|
tests.push(path);
|
|
718
1058
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
)
|
|
1059
|
+
}
|
|
1060
|
+
/** Each write reference's index in `found`, by its file, with the value it stores. */
|
|
1061
|
+
const valued = new Map<SourceFile, { index: number; value: Node }[]>();
|
|
1062
|
+
const holdsValue = (file: SourceFile, value: Node | undefined): void => {
|
|
1063
|
+
if (value !== undefined) {
|
|
1064
|
+
valued.set(file, [...(valued.get(file) ?? []), { index: found.length - 1, value }]);
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
for (const [use, sites] of [
|
|
1068
|
+
["write", contextual.writes],
|
|
1069
|
+
["read", contextual.reads],
|
|
1070
|
+
] as const) {
|
|
1071
|
+
for (const site of sites) {
|
|
1072
|
+
const file = site.at.getSourceFile();
|
|
1073
|
+
const position = renderPosition(file, root, site.at.getStart());
|
|
1074
|
+
for (const id of site.ids) {
|
|
1075
|
+
found.push({
|
|
1076
|
+
from: site.from,
|
|
1077
|
+
to: id,
|
|
1078
|
+
position,
|
|
1079
|
+
use,
|
|
1080
|
+
resolution: "contextual",
|
|
1081
|
+
test: testFile.get(file) === true,
|
|
1082
|
+
});
|
|
1083
|
+
if (use === "write") {
|
|
1084
|
+
holdsValue(file, site.at.parent);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
for (const { file, fileId, uses, reads, links, evaluations, decorated } of walked) {
|
|
1091
|
+
const test = testFile.get(file) === true;
|
|
1092
|
+
// A component file's markup may use any binding its blocks declare at the top level.
|
|
1093
|
+
const markup = isComponentFile(file);
|
|
1094
|
+
const bindings = markup ? importBindings(file) : [];
|
|
724
1095
|
|
|
725
1096
|
// A decorator is written inside the declaration it is attached to and receives it,
|
|
726
1097
|
// so the declaration is used where the decorator is written, whatever the
|
|
@@ -746,6 +1117,7 @@ export function references<Brand>(
|
|
|
746
1117
|
...uses.filter((site) => site.shorthand === undefined).map((site) => site.node),
|
|
747
1118
|
...links.map((link) => link.node),
|
|
748
1119
|
...evaluations.map((evaluation) => evaluation.node),
|
|
1120
|
+
...bindings.map((binding) => binding.node),
|
|
749
1121
|
]),
|
|
750
1122
|
];
|
|
751
1123
|
const answered = new Map<Node, Answer<TSSymbol>>();
|
|
@@ -809,6 +1181,49 @@ export function references<Brand>(
|
|
|
809
1181
|
}
|
|
810
1182
|
}
|
|
811
1183
|
|
|
1184
|
+
if (markup) {
|
|
1185
|
+
for (const symbol of declaredIn(fileId)) {
|
|
1186
|
+
found.push({
|
|
1187
|
+
from: fileId,
|
|
1188
|
+
to: symbol.id,
|
|
1189
|
+
position: symbol.position,
|
|
1190
|
+
use: "read",
|
|
1191
|
+
resolution: "syntax",
|
|
1192
|
+
test,
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
for (const binding of bindings) {
|
|
1196
|
+
const symbol = answered.get(binding.node);
|
|
1197
|
+
if (symbol === undefined) {
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
const position = renderPosition(file, root, binding.node.getStart());
|
|
1201
|
+
const chained =
|
|
1202
|
+
symbol === UNANSWERED
|
|
1203
|
+
? named(binding.node).map((id) => ({ id, stepped: false, guessed: true }))
|
|
1204
|
+
: chains.chainOf(symbol);
|
|
1205
|
+
// The markup may name any export of a module a binding holds whole.
|
|
1206
|
+
const targets = binding.whole
|
|
1207
|
+
? chained.flatMap((target) => [
|
|
1208
|
+
target,
|
|
1209
|
+
...declaredIn(target.id)
|
|
1210
|
+
.filter((one) => one.exported)
|
|
1211
|
+
.map((one) => ({ ...target, id: one.id })),
|
|
1212
|
+
])
|
|
1213
|
+
: chained;
|
|
1214
|
+
for (const target of targets) {
|
|
1215
|
+
found.push({
|
|
1216
|
+
from: fileId,
|
|
1217
|
+
to: target.id,
|
|
1218
|
+
position,
|
|
1219
|
+
use: "read",
|
|
1220
|
+
resolution: target.guessed ? "by-name" : target.stepped ? "alias" : "batch",
|
|
1221
|
+
test,
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
812
1227
|
// A pattern names a property of the value it destructures, which no name node of
|
|
813
1228
|
// the pattern resolves to: a shorthand's name is the local it binds.
|
|
814
1229
|
destructured.resolve(reads).forEach((symbol, index) => {
|
|
@@ -824,7 +1239,7 @@ export function references<Brand>(
|
|
|
824
1239
|
stepped: false,
|
|
825
1240
|
guessed: true,
|
|
826
1241
|
}))
|
|
827
|
-
:
|
|
1242
|
+
: targetsOf(symbol);
|
|
828
1243
|
for (const target of targets) {
|
|
829
1244
|
found.push({
|
|
830
1245
|
from: read.from,
|
|
@@ -851,10 +1266,11 @@ export function references<Brand>(
|
|
|
851
1266
|
const position = renderPosition(file, root, site.node.getStart());
|
|
852
1267
|
// A re-export something imports is used and so is the declaration behind it, so
|
|
853
1268
|
// the use names every link of the chain its name stands at the head of.
|
|
854
|
-
const targets =
|
|
1269
|
+
const targets = throughAliases(
|
|
855
1270
|
symbol === UNANSWERED
|
|
856
1271
|
? named(site.node).map((id) => ({ id, stepped: false, guessed: true }))
|
|
857
|
-
:
|
|
1272
|
+
: targetsOf(symbol),
|
|
1273
|
+
);
|
|
858
1274
|
for (const target of targets) {
|
|
859
1275
|
found.push({
|
|
860
1276
|
from: site.from,
|
|
@@ -864,6 +1280,20 @@ export function references<Brand>(
|
|
|
864
1280
|
resolution: target.guessed ? "by-name" : target.stepped ? "alias" : direct,
|
|
865
1281
|
test,
|
|
866
1282
|
});
|
|
1283
|
+
if (site.use === "write") {
|
|
1284
|
+
holdsValue(file, storedValue(site.node));
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
const usesOf = new Map(walked.map((one) => [one.file, one.uses]));
|
|
1291
|
+
for (const [file, writes] of valued) {
|
|
1292
|
+
const uses = usesOf.get(file) ?? [];
|
|
1293
|
+
for (const { index, names } of aloneIn(file, uses, writes, found)) {
|
|
1294
|
+
const reference = found[index];
|
|
1295
|
+
if (reference !== undefined) {
|
|
1296
|
+
found[index] = { ...reference, alone: names };
|
|
867
1297
|
}
|
|
868
1298
|
}
|
|
869
1299
|
}
|
|
@@ -891,6 +1321,8 @@ export function references<Brand>(
|
|
|
891
1321
|
aliasSteps: chains.steps,
|
|
892
1322
|
patternBatches: destructured.cost.patternBatches,
|
|
893
1323
|
patternLookups: destructured.cost.patternLookups,
|
|
1324
|
+
contextualBatches: contextual.batches,
|
|
1325
|
+
contextualLookups: contextual.lookups,
|
|
894
1326
|
},
|
|
895
1327
|
};
|
|
896
1328
|
}
|