@cplieger/deadset-ts 5.2.3 → 6.0.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 +28 -5
- package/dist/src/augmentations.js +50 -0
- package/dist/src/components.js +15 -15
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/convention-rows.js +17 -2
- package/dist/src/conventions.js +82 -1
- package/dist/src/dependencies.js +42 -26
- package/dist/src/entry-points.js +28 -14
- package/dist/src/exempt.js +1 -0
- package/dist/src/findings/completion.js +9 -4
- package/dist/src/findings/dependencies-and-module-machinery.js +2 -2
- package/dist/src/findings/emitters.js +64 -25
- package/dist/src/findings/interfaces.js +4 -3
- package/dist/src/findings/intra-function.js +15 -2
- package/dist/src/findings/reads-and-writes.js +3 -2
- package/dist/src/findings/unused-declarations.js +13 -1
- package/dist/src/findings/visibility-narrowing.js +2 -1
- package/dist/src/findings-pass.js +1 -0
- package/dist/src/graph.js +6 -6
- package/dist/src/implementations.js +1 -1
- package/dist/src/indexed-access.js +215 -0
- package/dist/src/interface-satisfaction.js +85 -23
- package/dist/src/intra-function-parts.js +139 -30
- package/dist/src/inventory.js +6 -1
- package/dist/src/kinds.js +13 -12
- package/dist/src/matrix.js +34 -9
- package/dist/src/module-declarations.js +180 -0
- package/dist/src/outside-tests.js +17 -15
- package/dist/src/overrides.js +70 -9
- package/dist/src/query.js +7 -0
- package/dist/src/ref.js +9 -0
- package/dist/src/references.js +6 -0
- package/dist/src/report.js +6 -1
- package/dist/src/reporters/sarif.js +6 -2
- package/dist/src/reporters/text.js +23 -3
- package/dist/src/roots.js +50 -10
- package/dist/src/suppress-file.js +3 -5
- package/dist/src/version.js +1 -1
- package/dist/src/workflow-steps.js +82 -0
- package/dist/src/workspace-programs.js +6 -31
- package/jsr.json +1 -1
- package/package.json +8 -2
- package/src/analysis.ts +40 -4
- package/src/augmentations.ts +72 -0
- package/src/components.ts +15 -13
- package/src/conformance-record.ts +2 -2
- package/src/convention-rows.ts +25 -3
- package/src/conventions.ts +106 -2
- package/src/dependencies.ts +51 -31
- package/src/entry-points.ts +32 -19
- package/src/exempt.ts +4 -1
- package/src/findings/completion.ts +11 -4
- package/src/findings/dependencies-and-module-machinery.ts +2 -2
- package/src/findings/emitters.ts +89 -27
- package/src/findings/interfaces.ts +7 -6
- package/src/findings/intra-function.ts +20 -2
- package/src/findings/reads-and-writes.ts +7 -2
- package/src/findings/unused-declarations.ts +16 -1
- package/src/findings/visibility-narrowing.ts +2 -1
- package/src/findings-pass.ts +4 -1
- package/src/graph.ts +6 -6
- package/src/implementations.ts +1 -1
- package/src/indexed-access.ts +295 -0
- package/src/interface-satisfaction.ts +118 -27
- package/src/intra-function-parts.ts +196 -35
- package/src/inventory.ts +8 -0
- package/src/kinds.ts +14 -12
- package/src/matrix.ts +51 -8
- package/src/module-declarations.ts +241 -0
- package/src/outside-tests.ts +18 -16
- package/src/overrides.ts +80 -14
- package/src/query.ts +28 -0
- package/src/ref.ts +10 -0
- package/src/references.ts +6 -0
- package/src/report.ts +11 -1
- package/src/reporters/sarif.ts +9 -2
- package/src/reporters/text.ts +25 -3
- package/src/roots.ts +56 -7
- package/src/suppress-file.ts +3 -6
- package/src/sweep.ts +12 -0
- package/src/version.ts +1 -1
- package/src/workflow-steps.ts +104 -0
- package/src/workspace-programs.ts +6 -35
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { evaluateEdges } from "../edges.js";
|
|
2
2
|
import { liveFilesAt } from "../matrix.js";
|
|
3
|
+
import { declarationKey } from "../ref.js";
|
|
3
4
|
import { ledgerOf } from "../suppress.js";
|
|
4
5
|
import { insideSkipped } from "../type-errors.js";
|
|
5
6
|
import { completed, dialsOf, reportable } from "./completion.js";
|
|
@@ -23,24 +24,24 @@ export const EMITTERS = new Map([
|
|
|
23
24
|
["intra-function", intraFunction],
|
|
24
25
|
]);
|
|
25
26
|
/**
|
|
26
|
-
* The
|
|
27
|
-
*
|
|
27
|
+
* The keys of every declaration an unanswered question could have kept live, which the run
|
|
28
|
+
* reports nothing about: a finding about one would be computed from the gap.
|
|
28
29
|
*/
|
|
29
|
-
function
|
|
30
|
+
function heldKeys(input) {
|
|
30
31
|
const held = new Set(input.swept.heldByUnanswered);
|
|
31
32
|
if (held.size === 0) {
|
|
32
33
|
return held;
|
|
33
34
|
}
|
|
34
35
|
return new Set(input.swept.matrix.union.symbols
|
|
35
36
|
.filter((symbol) => held.has(symbol.id))
|
|
36
|
-
.map((symbol) => symbol.ref));
|
|
37
|
+
.map((symbol) => declarationKey(symbol.ref, symbol.position.path)));
|
|
37
38
|
}
|
|
38
39
|
/**
|
|
39
|
-
* The
|
|
40
|
+
* The keys of every top-level declaration of a live component file. The file's
|
|
40
41
|
* markup may use each of them, which no analysis of the markup can rule out, so the run
|
|
41
42
|
* reports nothing about one while the file is live.
|
|
42
43
|
*/
|
|
43
|
-
function
|
|
44
|
+
function markupKeys(input) {
|
|
44
45
|
const { swept } = input;
|
|
45
46
|
if (swept.componentFiles.length === 0) {
|
|
46
47
|
return new Set();
|
|
@@ -48,7 +49,7 @@ function markupRefs(input) {
|
|
|
48
49
|
const live = new Set(liveFilesAt(swept.matrix, swept.sweep, new Set(swept.componentFiles)).map((symbol) => symbol.id));
|
|
49
50
|
return new Set(swept.matrix.union.symbols
|
|
50
51
|
.filter((symbol) => live.has(symbol.parent))
|
|
51
|
-
.map((symbol) => symbol.ref));
|
|
52
|
+
.map((symbol) => declarationKey(symbol.ref, symbol.position.path)));
|
|
52
53
|
}
|
|
53
54
|
/** Every family's findings over one sweep, and those of any further emitter, completed by one step. */
|
|
54
55
|
function completedOver(input, ...more) {
|
|
@@ -68,32 +69,70 @@ function dialsOver(input, found, suppressed, unmarked) {
|
|
|
68
69
|
};
|
|
69
70
|
}
|
|
70
71
|
/**
|
|
71
|
-
* Every finding of one run swept under its suppressions' marks, decided in one pass:
|
|
72
|
-
* family's findings completed by one step, so every component number is minted once,
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
72
|
+
* Every finding of one run swept under its suppressions' marks, decided in one pass: each
|
|
73
|
+
* family's findings completed by one step, so every component number is minted once, the
|
|
74
|
+
* finding each record claims withheld, the rest withheld by the dials, so a root one family
|
|
75
|
+
* reports withholds what another reports in its component, and every declared edge
|
|
76
|
+
* evaluated over what remains, so a pending finding is in its evaluation alone. What the
|
|
77
|
+
* minimum confidence withheld is what the same decision at the lowest minimum adds.
|
|
77
78
|
*/
|
|
78
79
|
export function decidedFindings(input, suppressed = { unmarked: input, records: [] }) {
|
|
79
80
|
const found = completedOver(input);
|
|
80
81
|
const unmarked = suppressed.records.length === 0 ? [] : completedOver(suppressed.unmarked, fallenMembers);
|
|
81
82
|
const would = [...unmarked, ...found];
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
83
|
+
const decided = dialledOver(input, suppressed, found, unmarked);
|
|
84
|
+
const shown = input.config.analysis.minConfidence === "possible"
|
|
85
|
+
? decided
|
|
86
|
+
: dialledOver(atPossible(input), {
|
|
87
|
+
unmarked: atPossible(suppressed.unmarked),
|
|
88
|
+
records: suppressed.records,
|
|
89
|
+
}, found, unmarked);
|
|
90
|
+
return { ...decided, would, withheld: withheldOf(decided.findings, shown.findings) };
|
|
91
|
+
}
|
|
92
|
+
/** The run's input with the minimum confidence at its lowest, which withholds nothing. */
|
|
93
|
+
function atPossible(input) {
|
|
94
|
+
const { config } = input;
|
|
90
95
|
return {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
ledger,
|
|
94
|
-
would,
|
|
96
|
+
...input,
|
|
97
|
+
config: { ...config, analysis: { ...config.analysis, minConfidence: "possible" } },
|
|
95
98
|
};
|
|
96
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* The run's completed findings decided under its dials: the finding each record claims
|
|
102
|
+
* withheld, the rest withheld by the dials, and every declared edge evaluated over what
|
|
103
|
+
* remains.
|
|
104
|
+
*/
|
|
105
|
+
function dialledOver(input, suppressed, found, unmarked) {
|
|
106
|
+
const would = [...unmarked, ...found];
|
|
107
|
+
const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
|
|
108
|
+
const unanswered = heldKeys(input);
|
|
109
|
+
const markup = markupKeys(input);
|
|
110
|
+
const kept = reportable(input, found.filter((finding) => {
|
|
111
|
+
const key = declarationKey(finding.symbol.ref, finding.position.path);
|
|
112
|
+
return (!ledger.withheld(finding) &&
|
|
113
|
+
!unanswered.has(key) &&
|
|
114
|
+
!markup.has(key) &&
|
|
115
|
+
!insideSkipped(input.swept.skipped, finding.position));
|
|
116
|
+
}));
|
|
117
|
+
const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
|
|
118
|
+
return { findings: evaluated.findings, evaluations: evaluated.evaluations, ledger };
|
|
119
|
+
}
|
|
120
|
+
/** The findings the lowest minimum would report that the run does not, counted by confidence. */
|
|
121
|
+
function withheldOf(reported, shown) {
|
|
122
|
+
const kept = new Set(reported.map(findingKey));
|
|
123
|
+
const counts = { certain: 0, probable: 0, possible: 0 };
|
|
124
|
+
for (const finding of shown) {
|
|
125
|
+
if (!kept.has(findingKey(finding))) {
|
|
126
|
+
counts[finding.confidence] += 1;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return counts;
|
|
130
|
+
}
|
|
131
|
+
/** A finding's identity across two decisions of one run: its position, code and subject. */
|
|
132
|
+
function findingKey(finding) {
|
|
133
|
+
const { path, line, column } = finding.position;
|
|
134
|
+
return JSON.stringify([path, line, column, finding.code, finding.symbol.ref]);
|
|
135
|
+
}
|
|
97
136
|
/** Every finding the run reports, each family's in table order. */
|
|
98
137
|
export function findingsOf(input) {
|
|
99
138
|
return decidedFindings(input).findings;
|
|
@@ -54,15 +54,16 @@ function findingOf(code, symbol, message, implementations) {
|
|
|
54
54
|
* The findings of the interfaces family, `DS1200` to `DS1299`: `DS1201`, a dead
|
|
55
55
|
* interface, whose members fall with it unreported; and `DS1203`, a method of a live
|
|
56
56
|
* interface no reference names, unless it is a marker, a method whose every
|
|
57
|
-
* implementation has an empty body. A test file's declaration is neither kind's
|
|
58
|
-
*
|
|
57
|
+
* implementation has an empty body. A test file's declaration is neither kind's subject,
|
|
58
|
+
* because a production sweep counts none of the references it can have, save an interface
|
|
59
|
+
* the run that counts test references does not reach, which is `DS1201`'s.
|
|
59
60
|
*/
|
|
60
61
|
export const interfaces = (input) => {
|
|
61
62
|
const run = indexed(input);
|
|
62
63
|
const found = [];
|
|
63
64
|
for (const candidate of input.swept.sweep.candidates) {
|
|
64
65
|
const symbol = run.symbolOf(candidate.id);
|
|
65
|
-
if (symbol === undefined || run.inTest(candidate.id)) {
|
|
66
|
+
if (symbol === undefined || (run.inTest(candidate.id) && !candidate.unreferencedTest)) {
|
|
66
67
|
continue;
|
|
67
68
|
}
|
|
68
69
|
if (symbol.kind === "interface") {
|
|
@@ -14,6 +14,19 @@ function overlapClause(overlap) {
|
|
|
14
14
|
? "no other rule is known to report it"
|
|
15
15
|
: `also reported by ${spelled(overlap)}`;
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Whether one parameter's name is reported: every unread one of a function whose
|
|
19
|
+
* signature is free, and of a function used as a value only one no read parameter follows,
|
|
20
|
+
* because a caller may pass such a function more arguments than it declares.
|
|
21
|
+
*/
|
|
22
|
+
function parameterReported(part, free, freeButValued) {
|
|
23
|
+
const place = part.parameter;
|
|
24
|
+
if (place?.inValue !== true && free.has(part.declaration)) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
const valued = place?.inValue === true || freeButValued.has(part.declaration);
|
|
28
|
+
return valued && place?.beforeARead !== true;
|
|
29
|
+
}
|
|
17
30
|
/**
|
|
18
31
|
* Whether one part is reported. A parameter needs its function's signature free to
|
|
19
32
|
* change; a result needs that, every call in the loaded program to discard it, and every
|
|
@@ -25,10 +38,10 @@ function reported(input, part, callersUnknown) {
|
|
|
25
38
|
if (part.used) {
|
|
26
39
|
return false;
|
|
27
40
|
}
|
|
28
|
-
const { free, discardedEverywhere } = input.intraFunction;
|
|
41
|
+
const { free, freeButValued, discardedEverywhere } = input.intraFunction;
|
|
29
42
|
switch (part.code) {
|
|
30
43
|
case UNUSED_PARAMETER:
|
|
31
|
-
return
|
|
44
|
+
return parameterReported(part, free, freeButValued);
|
|
32
45
|
case UNUSED_RESULT:
|
|
33
46
|
return (free.has(part.declaration) &&
|
|
34
47
|
discardedEverywhere.has(part.declaration) &&
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { positionKey } from "../position.js";
|
|
2
|
+
import { declarationKey } from "../ref.js";
|
|
2
3
|
/** The containers whose type parameters `DS1303` reports: a function and a class method. */
|
|
3
4
|
const SIGNATURES = new Set(["function", "method"]);
|
|
4
5
|
function indexed(input) {
|
|
@@ -175,12 +176,12 @@ function unusedTypeParameters(run) {
|
|
|
175
176
|
.map((symbol) => findingAbout(symbol, "DS1303", `type parameter ${symbol.name} is named in neither its signature nor its body`));
|
|
176
177
|
}
|
|
177
178
|
/**
|
|
178
|
-
* The
|
|
179
|
+
* The keys of the declarations the write-only kind reports, whatever the
|
|
179
180
|
* severity: a narrowing kind yields each to it, since deleting the declaration
|
|
180
181
|
* supersedes narrowing it.
|
|
181
182
|
*/
|
|
182
183
|
export function writeOnlyDeclarations(input) {
|
|
183
|
-
return new Set(writeOnlySymbols(indexed(input)).map((finding) => finding.symbol.ref));
|
|
184
|
+
return new Set(writeOnlySymbols(indexed(input)).map((finding) => declarationKey(finding.symbol.ref, finding.position.path)));
|
|
184
185
|
}
|
|
185
186
|
/**
|
|
186
187
|
* The findings of the reads-and-writes family, `DS1300` to `DS1399`, in code order and
|
|
@@ -49,9 +49,14 @@ function codeOf(judged) {
|
|
|
49
49
|
if (candidate.testOfDeadCode) {
|
|
50
50
|
return "DS1005";
|
|
51
51
|
}
|
|
52
|
-
if (judged.test
|
|
52
|
+
if ((judged.test && !candidate.unreferencedTest) ||
|
|
53
|
+
judged.containerDead ||
|
|
54
|
+
NOT_THIS_FAMILY.has(symbol.kind)) {
|
|
53
55
|
return undefined;
|
|
54
56
|
}
|
|
57
|
+
if (candidate.unreferencedTest) {
|
|
58
|
+
return unreferencedCode(symbol);
|
|
59
|
+
}
|
|
55
60
|
const referenced = candidate.productionRefs + candidate.testRefs > 0;
|
|
56
61
|
if (symbol.kind === "enum-member" && !referenced) {
|
|
57
62
|
return undefined;
|
|
@@ -73,6 +78,13 @@ function codeOf(judged) {
|
|
|
73
78
|
}
|
|
74
79
|
return judged.unreachable ? undefined : "DS1001";
|
|
75
80
|
}
|
|
81
|
+
/** The code a declaration nothing references carries, by its shape and its visibility. */
|
|
82
|
+
function unreferencedCode(symbol) {
|
|
83
|
+
if (MEMBERS.has(symbol.kind)) {
|
|
84
|
+
return "DS1003";
|
|
85
|
+
}
|
|
86
|
+
return symbol.exported ? "DS1001" : "DS1002";
|
|
87
|
+
}
|
|
76
88
|
/** The visibility a member's message names, where it narrows the member's reach. */
|
|
77
89
|
function visibilityWord(symbol) {
|
|
78
90
|
switch (symbol.visibility) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { OUTSIDE } from "../graph.js";
|
|
2
|
+
import { declarationKey } from "../ref.js";
|
|
2
3
|
import { writeOnlyDeclarations } from "./reads-and-writes.js";
|
|
3
4
|
const UNNECESSARY_EXPORT = "DS1101";
|
|
4
5
|
const UNREACHABLE_EXPORT = "DS1103";
|
|
@@ -119,7 +120,7 @@ function narrowing(facts, at, symbol, world) {
|
|
|
119
120
|
const { closed } = world;
|
|
120
121
|
const file = facts.fileOf[at] ?? OUTSIDE;
|
|
121
122
|
if (!topLevelExport(facts, at, symbol) ||
|
|
122
|
-
world.writeOnly.has(symbol.ref) ||
|
|
123
|
+
world.writeOnly.has(declarationKey(symbol.ref, symbol.position.path)) ||
|
|
123
124
|
symbol.kind === "export-alias" ||
|
|
124
125
|
entersByName(facts.rootedBy[at]) ||
|
|
125
126
|
entersByName(facts.rootedBy[file]) ||
|
package/dist/src/graph.js
CHANGED
|
@@ -32,11 +32,11 @@ const SWEPT = {
|
|
|
32
32
|
};
|
|
33
33
|
/**
|
|
34
34
|
* Whether a root of each kind names a caller, which holds it live under both relations: a
|
|
35
|
-
* tool reads
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
35
|
+
* tool reads what it loads and what its strings name, a runtime runs a test, setup or
|
|
36
|
+
* worker file, a page's module script, a command, a script or a workflow step, the manifest
|
|
37
|
+
* names what enters the package, an alias global stands for its export, and an entry file,
|
|
38
|
+
* a configured root and a pattern assert a caller. A library's published API only supposes
|
|
39
|
+
* an unseen consumer, so it is live under reachability alone.
|
|
40
40
|
*/
|
|
41
41
|
const NAMES_A_CALLER = {
|
|
42
42
|
"entry-file": true,
|
|
@@ -52,10 +52,10 @@ const NAMES_A_CALLER = {
|
|
|
52
52
|
"configuration-file": true,
|
|
53
53
|
"configuration-string": true,
|
|
54
54
|
"test-runner": true,
|
|
55
|
-
"mutation-testing": true,
|
|
56
55
|
"browser-tests": true,
|
|
57
56
|
worker: true,
|
|
58
57
|
"html-entry": true,
|
|
58
|
+
"workflow-step": true,
|
|
59
59
|
};
|
|
60
60
|
/** Whether the sweep judges a declaration of one kind. */
|
|
61
61
|
export function swept(kind) {
|
|
@@ -54,7 +54,7 @@ function typesAt(project, nodes) {
|
|
|
54
54
|
return typeAt;
|
|
55
55
|
}
|
|
56
56
|
/** Whether one class member is a method whose every written body holds no statement. */
|
|
57
|
-
function emptyImplementation(project, member) {
|
|
57
|
+
export function emptyImplementation(project, member) {
|
|
58
58
|
const own = project.ownPaths();
|
|
59
59
|
let bodies = 0;
|
|
60
60
|
for (const handle of member.declarations) {
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The members an indexed-access type names. `T["a" | "b"]` reads the members `a` and `b`
|
|
3
|
+
* of `T` as a property access by either name would. Where the index is a type parameter
|
|
4
|
+
* a call fixes, the call names the members the literal type argument names, written or
|
|
5
|
+
* inferred, which is read off the parameter the type parameter annotates whole. The type
|
|
6
|
+
* query of a type-query alias is no reference.
|
|
7
|
+
*/
|
|
8
|
+
import { isArrowFunction, isCallExpression, isFunctionDeclaration, isFunctionExpression, isIdentifier, isIndexedAccessTypeNode, isLiteralTypeNode, isMethodDeclaration, isNewExpression, isNoSubstitutionTemplateLiteral, isNumericLiteral, isParenthesizedTypeNode, isPropertyAccessExpression, isStringLiteral, isTypeReferenceNode, isUnionTypeNode, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
9
|
+
import { nodeKey } from "./inventory.js";
|
|
10
|
+
import { renderPosition } from "./position.js";
|
|
11
|
+
import { UNANSWERED } from "./query.js";
|
|
12
|
+
import { typeQueryAliases } from "./type-query-alias.js";
|
|
13
|
+
function isGeneric(node) {
|
|
14
|
+
return ((isFunctionDeclaration(node) ||
|
|
15
|
+
isMethodDeclaration(node) ||
|
|
16
|
+
isArrowFunction(node) ||
|
|
17
|
+
isFunctionExpression(node)) &&
|
|
18
|
+
node.typeParameters !== undefined);
|
|
19
|
+
}
|
|
20
|
+
function unwrappedType(node) {
|
|
21
|
+
return isParenthesizedTypeNode(node) ? unwrappedType(node.type) : node;
|
|
22
|
+
}
|
|
23
|
+
/** The names a literal index type spells, or none where the index is any other type. */
|
|
24
|
+
function literalNames(index) {
|
|
25
|
+
const node = unwrappedType(index);
|
|
26
|
+
if (isUnionTypeNode(node)) {
|
|
27
|
+
const names = node.types.map(literalNames);
|
|
28
|
+
return names.every((one) => one !== undefined) ? names.flat() : undefined;
|
|
29
|
+
}
|
|
30
|
+
if (!isLiteralTypeNode(node)) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
const literal = node.literal;
|
|
34
|
+
if (isStringLiteral(literal) || isNoSubstitutionTemplateLiteral(literal)) {
|
|
35
|
+
return [literal.text];
|
|
36
|
+
}
|
|
37
|
+
return isNumericLiteral(literal) ? [String(Number(literal.text))] : undefined;
|
|
38
|
+
}
|
|
39
|
+
/** The name a literal type carries, a union's each, or none where a constituent is no literal. */
|
|
40
|
+
function literalTypeNames(type, parts) {
|
|
41
|
+
const names = [];
|
|
42
|
+
for (const part of parts(type)) {
|
|
43
|
+
if (part.isStringLiteralType()) {
|
|
44
|
+
names.push(part.value);
|
|
45
|
+
}
|
|
46
|
+
else if (part.isNumberLiteralType()) {
|
|
47
|
+
names.push(String(part.value));
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return names;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The references the indexed-access types of one project's own files make to the members
|
|
57
|
+
* they name, each made by the declaration that holds the type or the call, or by the file.
|
|
58
|
+
*/
|
|
59
|
+
export function indexedAccessReferences(project, held, chains, root, testFiles) {
|
|
60
|
+
const queries = project.queries;
|
|
61
|
+
const accesses = [];
|
|
62
|
+
const generics = new Map();
|
|
63
|
+
const calls = [];
|
|
64
|
+
const names = new Set();
|
|
65
|
+
for (const file of project.ownSourceFiles()) {
|
|
66
|
+
const fileId = held.declarations.get(nodeKey(file, file));
|
|
67
|
+
if (fileId === undefined) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const aliasTypes = new Set(typeQueryAliases(file).map((alias) => alias.type));
|
|
71
|
+
const visit = (node, from, enclosing) => {
|
|
72
|
+
const own = held.declarations.get(nodeKey(file, node));
|
|
73
|
+
const holder = own ?? from;
|
|
74
|
+
const inside = isGeneric(node) ? [...enclosing, node] : enclosing;
|
|
75
|
+
if (isIndexedAccessTypeNode(node) && !aliasTypes.has(node)) {
|
|
76
|
+
const literal = literalNames(node.indexType);
|
|
77
|
+
if (literal !== undefined) {
|
|
78
|
+
accesses.push({ file, from: holder, at: node, object: node.objectType, names: literal });
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
parameterIndex(node, inside, file, generics, names);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (isCallExpression(node) || isNewExpression(node)) {
|
|
85
|
+
const callee = node.expression;
|
|
86
|
+
const name = isIdentifier(callee)
|
|
87
|
+
? callee.text
|
|
88
|
+
: isPropertyAccessExpression(callee) && isIdentifier(callee.name)
|
|
89
|
+
? callee.name.text
|
|
90
|
+
: undefined;
|
|
91
|
+
if (name !== undefined) {
|
|
92
|
+
calls.push({ file, from: holder, call: node, name });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
node.forEachChild((child) => {
|
|
96
|
+
visit(child, holder, inside);
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
file.forEachChild((child) => {
|
|
100
|
+
visit(child, fileId, []);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
for (const { file, from, call, name } of calls) {
|
|
104
|
+
if (!names.has(name)) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const resolved = queries.resolvedSignature(call);
|
|
108
|
+
if (resolved === UNANSWERED || resolved?.declaration === undefined) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const declaration = project.declarationAt(resolved.declaration)?.node;
|
|
112
|
+
const byParameter = declaration === undefined
|
|
113
|
+
? undefined
|
|
114
|
+
: generics.get(nodeKey(declaration.getSourceFile(), declaration));
|
|
115
|
+
if (byParameter === undefined) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
for (const { object, parameter } of byParameter) {
|
|
119
|
+
const fixed = queries.typeAtPosition(resolved, parameter);
|
|
120
|
+
const literal = fixed === UNANSWERED ? undefined : literalTypeNames(fixed, (type) => partsOf(type));
|
|
121
|
+
if (literal !== undefined) {
|
|
122
|
+
accesses.push({ file, from, at: call, object, names: literal });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function partsOf(type) {
|
|
127
|
+
const parts = queries.constituents(type);
|
|
128
|
+
return parts === UNANSWERED ? [type] : parts;
|
|
129
|
+
}
|
|
130
|
+
const objectNodes = accesses.flatMap((one) => ("kind" in one.object ? [one.object] : []));
|
|
131
|
+
const typeOf = new Map();
|
|
132
|
+
queries.typesAt(objectNodes).forEach((answer, index) => {
|
|
133
|
+
const node = objectNodes[index];
|
|
134
|
+
if (answer !== UNANSWERED && answer !== undefined && node !== undefined) {
|
|
135
|
+
typeOf.set(node, answer);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
const found = [];
|
|
139
|
+
for (const access of accesses) {
|
|
140
|
+
const type = "kind" in access.object ? typeOf.get(access.object) : access.object;
|
|
141
|
+
if (type === undefined) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const position = renderPosition(access.file, root, access.at.getStart());
|
|
145
|
+
for (const part of partsOf(type)) {
|
|
146
|
+
const properties = queries.propertiesOf(part);
|
|
147
|
+
if (properties === UNANSWERED) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
for (const property of properties) {
|
|
151
|
+
if (!access.names.includes(property.escapedName)) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
for (const to of chains.declarationsOf(property)) {
|
|
155
|
+
found.push({
|
|
156
|
+
from: access.from,
|
|
157
|
+
to,
|
|
158
|
+
position,
|
|
159
|
+
use: "read",
|
|
160
|
+
resolution: "batch",
|
|
161
|
+
test: testFiles.has(position.path),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return found;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Records an indexed-access type whose index names a type parameter of an enclosing
|
|
171
|
+
* function that a parameter of that function is annotated with whole, so a call's
|
|
172
|
+
* argument for that parameter fixes the index.
|
|
173
|
+
*/
|
|
174
|
+
function parameterIndex(node, enclosing, file, generics, names) {
|
|
175
|
+
const index = unwrappedType(node.indexType);
|
|
176
|
+
if (!isTypeReferenceNode(index) || !isIdentifier(index.typeName)) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const spelled = index.typeName.text;
|
|
180
|
+
for (const generic of [...enclosing].reverse()) {
|
|
181
|
+
const declares = (generic.typeParameters ?? []).some((parameter) => isIdentifier(parameter.name) && parameter.name.text === spelled);
|
|
182
|
+
if (!declares) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const at = generic.parameters.findIndex((parameter) => {
|
|
186
|
+
const type = parameter.type === undefined ? undefined : unwrappedType(parameter.type);
|
|
187
|
+
return (type !== undefined &&
|
|
188
|
+
isTypeReferenceNode(type) &&
|
|
189
|
+
isIdentifier(type.typeName) &&
|
|
190
|
+
type.typeName.text === spelled);
|
|
191
|
+
});
|
|
192
|
+
if (at >= 0) {
|
|
193
|
+
const key = nodeKey(file, generic);
|
|
194
|
+
generics.set(key, [...(generics.get(key) ?? []), { object: node.objectType, parameter: at }]);
|
|
195
|
+
const name = calledName(generic);
|
|
196
|
+
if (name !== undefined) {
|
|
197
|
+
names.add(name);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** The name a call writes to call one generic function: its own, or its variable's. */
|
|
204
|
+
function calledName(generic) {
|
|
205
|
+
const own = generic.name;
|
|
206
|
+
if (own !== undefined && isIdentifier(own)) {
|
|
207
|
+
return own.text;
|
|
208
|
+
}
|
|
209
|
+
const parent = generic.parent;
|
|
210
|
+
return parent.kind === SyntaxKind.VariableDeclaration &&
|
|
211
|
+
parent.name !== undefined &&
|
|
212
|
+
isIdentifier(parent.name)
|
|
213
|
+
? parent.name.text
|
|
214
|
+
: undefined;
|
|
215
|
+
}
|