@cplieger/deadset-ts 8.0.0-dev.16 → 8.0.0-dev.18
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/alias-chain.js +0 -5
- package/dist/src/amd.js +81 -0
- package/dist/src/analysis.js +82 -91
- package/dist/src/augmentations.js +24 -25
- package/dist/src/bindings.js +382 -0
- package/dist/src/calls.js +279 -43
- package/dist/src/commands/tools.js +5 -9
- package/dist/src/component-files.js +3 -2
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/consumers.js +6 -9
- package/dist/src/contextual-uses.js +1 -17
- package/dist/src/contract/config.generated.js +4 -1
- package/dist/src/contract/contract.generated.js +2 -2
- package/dist/src/contract/report.generated.js +6 -2
- package/dist/src/dependencies.js +25 -29
- package/dist/src/destructuring.js +1 -19
- package/dist/src/element-access.js +179 -164
- package/dist/src/entries/build-scripts.js +2 -2
- package/dist/src/entries/configuration-files.js +3 -7
- package/dist/src/entries/conventions.js +2 -2
- package/dist/src/entries/html-entries.js +7 -10
- package/dist/src/entries/mdx-entries.js +2 -2
- package/dist/src/entries/static-module.js +2 -1
- package/dist/src/exempt.js +0 -26
- package/dist/src/exposure.js +400 -0
- package/dist/src/facts.js +4 -4
- package/dist/src/findings/completion.js +44 -21
- package/dist/src/findings/emitters.js +22 -33
- package/dist/src/{implementations.js → findings/implementations.js} +3 -3
- package/dist/src/{in-operands.js → findings/in-operands.js} +2 -2
- package/dist/src/findings/intra-function.js +3 -3
- package/dist/src/findings/reads-and-writes.js +71 -15
- package/dist/src/findings/unused-declarations.js +2 -22
- package/dist/src/findings/visibility-narrowing.js +14 -20
- package/dist/src/framework-lifecycle.js +41 -14
- package/dist/src/import-bindings.js +73 -28
- package/dist/src/index/sites.js +557 -0
- package/dist/src/indexed-access.js +523 -478
- package/dist/src/interface-satisfaction.js +1 -1
- package/dist/src/inventory.js +18 -0
- package/dist/src/keyof-comparisons.js +124 -103
- package/dist/src/{components.js → liveness/components.js} +1 -1
- package/dist/src/{graph.js → liveness/graph.js} +45 -20
- package/dist/src/liveness/matrix.js +99 -0
- package/dist/src/liveness/sweep.js +558 -0
- package/dist/src/load/expected-module.js +2 -2
- package/dist/src/load/specifiers.js +2 -4
- package/dist/src/load/synthesize.js +5 -0
- package/dist/src/load/workspace-resolution.js +2 -2
- package/dist/src/module-declarations.js +70 -85
- package/dist/src/overrides.js +134 -112
- package/dist/src/paths.js +46 -7
- package/dist/src/platform-calls.js +2 -2
- package/dist/src/platform-symbols.js +13 -19
- package/dist/src/query.js +46 -0
- package/dist/src/references/read-rule.js +1 -0
- package/dist/src/references/read-rules.js +48 -0
- package/dist/src/references/sink.js +237 -0
- package/dist/src/references/sources.js +450 -0
- package/dist/src/references.js +110 -1285
- package/dist/src/roots/configured-declarations.js +3 -0
- package/dist/src/roots/published-reach.js +4 -4
- package/dist/src/roots/root-set.js +1 -1
- package/dist/src/roots/unseen-loads.js +38 -10
- package/dist/src/serialization-contract.js +8 -3
- package/dist/src/stores.js +4 -2
- package/dist/src/symbol-keys.js +89 -83
- package/dist/src/tests/file-class.js +76 -0
- package/dist/src/{outside-tests.js → tests/outside.js} +7 -7
- package/dist/src/{test-support.js → tests/support.js} +2 -11
- package/dist/src/type-parameter-reads.js +178 -139
- package/dist/src/unimplemented-callees.js +262 -242
- package/dist/src/verbs/analyze.js +3 -3
- package/dist/src/verbs/explain.js +75 -132
- package/dist/src/verbs/print-retained.js +3 -3
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/alias-chain.ts +0 -7
- package/src/amd.ts +130 -0
- package/src/analysis.ts +125 -128
- package/src/augmentations.ts +42 -28
- package/src/bindings.ts +460 -0
- package/src/calls.ts +338 -46
- package/src/commands/tools.ts +5 -11
- package/src/component-files.ts +4 -2
- package/src/conformance-record.ts +1 -1
- package/src/consumers.ts +8 -11
- package/src/contextual-uses.ts +2 -22
- package/src/contract/config.generated.ts +4 -1
- package/src/contract/contract.generated.ts +2 -2
- package/src/contract/report.generated.ts +6 -2
- package/src/dependencies.ts +27 -28
- package/src/destructuring.ts +1 -31
- package/src/element-access.ts +188 -182
- package/src/entries/build-scripts.ts +2 -2
- package/src/entries/configuration-files.ts +11 -8
- package/src/entries/conventions.ts +2 -1
- package/src/entries/html-entries.ts +15 -14
- package/src/entries/mdx-entries.ts +2 -2
- package/src/entries/static-module.ts +2 -1
- package/src/exempt.ts +31 -46
- package/src/exposure.ts +492 -0
- package/src/facts.ts +5 -3
- package/src/finding.ts +1 -1
- package/src/findings/boundary.ts +9 -0
- package/src/findings/completion.ts +48 -23
- package/src/findings/emitter.ts +1 -1
- package/src/findings/emitters.ts +23 -41
- package/src/{implementations.ts → findings/implementations.ts} +4 -4
- package/src/{in-operands.ts → findings/in-operands.ts} +3 -3
- package/src/findings/interfaces.ts +3 -3
- package/src/findings/intra-function.ts +3 -3
- package/src/findings/reads-and-writes.ts +99 -26
- package/src/findings/unused-declarations.ts +4 -28
- package/src/findings/visibility-narrowing.ts +25 -26
- package/src/framework-lifecycle.ts +42 -14
- package/src/import-bindings.ts +98 -29
- package/src/index/sites.ts +781 -0
- package/src/indexed-access.ts +570 -527
- package/src/interface-satisfaction.ts +2 -2
- package/src/inventory.ts +19 -0
- package/src/keyof-comparisons.ts +131 -116
- package/src/{components.ts → liveness/components.ts} +3 -3
- package/src/{graph.ts → liveness/graph.ts} +93 -27
- package/src/liveness/matrix.ts +162 -0
- package/src/liveness/sweep.ts +855 -0
- package/src/load/expected-module.ts +8 -2
- package/src/load/specifiers.ts +8 -5
- package/src/load/synthesize.ts +6 -0
- package/src/load/workspace-resolution.ts +9 -2
- package/src/module-declarations.ts +74 -98
- package/src/overrides.ts +162 -134
- package/src/paths.ts +54 -8
- package/src/platform-calls.ts +3 -3
- package/src/platform-symbols.ts +11 -27
- package/src/query.ts +81 -0
- package/src/references/read-rule.ts +60 -0
- package/src/references/read-rules.ts +55 -0
- package/src/references/sink.ts +314 -0
- package/src/references/sources.ts +632 -0
- package/src/references.ts +175 -1733
- package/src/report.ts +1 -1
- package/src/roots/configured-declarations.ts +3 -0
- package/src/roots/published-reach.ts +9 -4
- package/src/roots/root-set.ts +1 -1
- package/src/roots/unseen-loads.ts +49 -11
- package/src/serialization-contract.ts +8 -3
- package/src/stores.ts +15 -3
- package/src/symbol-keys.ts +102 -102
- package/src/tests/file-class.ts +135 -0
- package/src/{outside-tests.ts → tests/outside.ts} +16 -10
- package/src/{test-support.ts → tests/support.ts} +9 -20
- package/src/type-parameter-reads.ts +187 -158
- package/src/unimplemented-callees.ts +297 -280
- package/src/verbs/analyze.ts +3 -2
- package/src/verbs/explain.ts +83 -158
- package/src/verbs/print-retained.ts +3 -3
- package/dist/src/matrix.js +0 -273
- package/dist/src/sweep.js +0 -222
- package/src/matrix.ts +0 -463
- package/src/sweep.ts +0 -361
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* together with every finding that falls with a root they withhold.
|
|
5
5
|
*/
|
|
6
6
|
import { CODE, FAMILY_KEY_LENGTH, KIND_ROWS, } from "../contract/contract.generated.js";
|
|
7
|
-
import { componentId } from "../components.js";
|
|
8
|
-
import { deadComponents } from "../
|
|
9
|
-
import { OUTSIDE } from "../graph.js";
|
|
7
|
+
import { componentId } from "../liveness/components.js";
|
|
8
|
+
import { deadComponents } from "../liveness/sweep.js";
|
|
9
|
+
import { OUTSIDE } from "../liveness/graph.js";
|
|
10
10
|
import { KINDS } from "../kinds.js";
|
|
11
11
|
import { byPosition, positionKey } from "../position.js";
|
|
12
12
|
import { declarationKey } from "../ref.js";
|
|
@@ -79,12 +79,13 @@ function testedByIn(input) {
|
|
|
79
79
|
* published API, a public member of a published type included, is `certain` where the
|
|
80
80
|
* run loaded every consumer the scope declared and declared one, `probable` where one did
|
|
81
81
|
* not load, and `possible` where it declared none, whatever the configuration says of the
|
|
82
|
-
* consumer set. A member an `in` expression tests by name,
|
|
83
|
-
* analysis fills, is `possible`.
|
|
82
|
+
* consumer set. A member an `in` expression tests by name, one a function outside the
|
|
83
|
+
* analysis fills, and one {@link possiblyLoaded} names, is `possible`.
|
|
84
84
|
*/
|
|
85
|
-
function reachabilityClasses(input) {
|
|
85
|
+
export function reachabilityClasses(input) {
|
|
86
86
|
const tested = testedByIn(input);
|
|
87
87
|
const filled = input.boundary.filledOutside ?? new Set();
|
|
88
|
+
const possible = possiblyLoaded(input);
|
|
88
89
|
const published = publishedIds(input.swept.matrix.union);
|
|
89
90
|
const { declared } = input.boundary.consumers;
|
|
90
91
|
const outside = declared.length === 0
|
|
@@ -93,25 +94,12 @@ function reachabilityClasses(input) {
|
|
|
93
94
|
? "certain"
|
|
94
95
|
: "probable";
|
|
95
96
|
return (id) => {
|
|
96
|
-
if (tested.has(id) || filled.has(id)) {
|
|
97
|
+
if (tested.has(id) || filled.has(id) || possible.has(id)) {
|
|
97
98
|
return "possible";
|
|
98
99
|
}
|
|
99
100
|
return published.has(id) ? outside : "certain";
|
|
100
101
|
};
|
|
101
102
|
}
|
|
102
|
-
/**
|
|
103
|
-
* Whether one test of dead code references a target symbol that is published API of a run
|
|
104
|
-
* that declares no consumer, which outside programs the run cannot see may use, so the
|
|
105
|
-
* target counts as live for the test and the test is no test of dead code.
|
|
106
|
-
*/
|
|
107
|
-
export function testsUnseenAPI(input) {
|
|
108
|
-
if (input.boundary.consumers.declared.length > 0) {
|
|
109
|
-
return () => false;
|
|
110
|
-
}
|
|
111
|
-
const union = input.swept.matrix.union;
|
|
112
|
-
const published = publishedIds(union);
|
|
113
|
-
return (id) => testTargets(union, id).some((target) => published.has(target));
|
|
114
|
-
}
|
|
115
103
|
/** The target symbols one test of dead code references, by id: production declarations the sweep judges. */
|
|
116
104
|
function testTargets(union, id) {
|
|
117
105
|
return (union.out[union.at(id)] ?? []).flatMap((edge) => {
|
|
@@ -137,6 +125,35 @@ function testCeilings(input, classOf) {
|
|
|
137
125
|
? testTargets(union, id).reduce((lowest, target) => capped(lowest, classOf(target)), "certain")
|
|
138
126
|
: "certain";
|
|
139
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* What only a bundler's retry of an import Node leaves unresolved names (program.38), and each
|
|
130
|
+
* dead declaration that reaches through dead declarations, as confidence.20 reads what only
|
|
131
|
+
* tests reach: each is dead only while no loader the analysis does not see loads the file.
|
|
132
|
+
*/
|
|
133
|
+
function possiblyLoaded(input) {
|
|
134
|
+
const seeds = input.boundary.possiblyLoaded ?? new Set();
|
|
135
|
+
if (seeds.size === 0) {
|
|
136
|
+
return seeds;
|
|
137
|
+
}
|
|
138
|
+
const union = input.swept.matrix.union;
|
|
139
|
+
const dead = new Set(input.swept.sweep.candidates.map((one) => one.id));
|
|
140
|
+
const reached = new Set();
|
|
141
|
+
const pending = union.symbols.flatMap((symbol, at) => (seeds.has(symbol.id) ? [at] : []));
|
|
142
|
+
for (let at = pending.pop(); at !== undefined; at = pending.pop()) {
|
|
143
|
+
const symbol = union.symbols[at];
|
|
144
|
+
if (symbol === undefined || reached.has(symbol.id)) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
reached.add(symbol.id);
|
|
148
|
+
for (const edge of union.out[at] ?? []) {
|
|
149
|
+
const to = union.symbols[edge.to];
|
|
150
|
+
if (to !== undefined && dead.has(to.id)) {
|
|
151
|
+
pending.push(edge.to);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return reached;
|
|
156
|
+
}
|
|
140
157
|
/**
|
|
141
158
|
* The subjects dead only because production code only tests use is: each declaration the
|
|
142
159
|
* sweep marks test-used, each `DS1602` file of such code, and each test of dead code every
|
|
@@ -281,6 +298,7 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
281
298
|
const union = swept.matrix.union;
|
|
282
299
|
const positions = new Map(union.symbols.map((symbol, at) => [declarationKey(symbol.ref, symbol.position.path), at]));
|
|
283
300
|
const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
|
|
301
|
+
const fileIds = new Map(union.symbols.flatMap((one) => (one.kind === "file" ? [[one.position.path, one.id]] : [])));
|
|
284
302
|
const classOf = reachabilityClasses(input);
|
|
285
303
|
const ceilingOf = testCeilings(input, classOf);
|
|
286
304
|
const testUsed = testUsedSubjects(input);
|
|
@@ -365,7 +383,12 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
365
383
|
const counts = candidate === undefined
|
|
366
384
|
? union.made[at]
|
|
367
385
|
: { production: candidate.productionRefs, test: candidate.testRefs };
|
|
368
|
-
const ownClass = judged
|
|
386
|
+
const ownClass = judged !== undefined
|
|
387
|
+
? classOf(judged.id)
|
|
388
|
+
: (finding.reachabilityClass ??
|
|
389
|
+
(finding.code === CODE.fileNeverImported
|
|
390
|
+
? classOf(fileIds.get(finding.position.path) ?? "")
|
|
391
|
+
: "certain"));
|
|
369
392
|
const used = judged !== undefined
|
|
370
393
|
? candidate !== undefined && testUsed.declarations.has(judged.id)
|
|
371
394
|
: finding.code === CODE.fileNeverImported && testUsed.files.has(finding.position.path);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { evaluateEdges } from "../edges.js";
|
|
2
|
-
import { liveFilesAt } from "../matrix.js";
|
|
3
2
|
import { declarationKey } from "../ref.js";
|
|
4
3
|
import { ledgerOf } from "../suppress.js";
|
|
5
4
|
import { insideSkipped } from "../type-errors.js";
|
|
@@ -8,7 +7,7 @@ import { dependenciesAndModuleMachinery } from "./dependencies-and-module-machin
|
|
|
8
7
|
import { interfaces } from "./interfaces.js";
|
|
9
8
|
import { intraFunction } from "./intra-function.js";
|
|
10
9
|
import { nonCodeArtifacts } from "./non-code-artifacts.js";
|
|
11
|
-
import { readsAndWrites } from "./reads-and-writes.js";
|
|
10
|
+
import { fallingWithWrites, readsAndWrites } from "./reads-and-writes.js";
|
|
12
11
|
import { selfCheck } from "./self-check.js";
|
|
13
12
|
import { fallenMembers, unusedDeclarations } from "./unused-declarations.js";
|
|
14
13
|
import { visibilityNarrowing } from "./visibility-narrowing.js";
|
|
@@ -23,37 +22,25 @@ export const EMITTERS = new Map([
|
|
|
23
22
|
["self-check", selfCheck],
|
|
24
23
|
["intra-function", intraFunction],
|
|
25
24
|
]);
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
*/
|
|
30
|
-
function heldKeys(input) {
|
|
31
|
-
const held = new Set(input.swept.heldByUnanswered);
|
|
32
|
-
if (held.size === 0) {
|
|
33
|
-
return held;
|
|
34
|
-
}
|
|
25
|
+
/** The keys of the declarations the identifiers name, as {@link findingKey} keys a finding. */
|
|
26
|
+
function keysOf(input, ids) {
|
|
27
|
+
const named = new Set(ids);
|
|
35
28
|
return new Set(input.swept.matrix.union.symbols
|
|
36
|
-
.filter((symbol) =>
|
|
29
|
+
.filter((symbol) => named.has(symbol.id))
|
|
37
30
|
.map((symbol) => declarationKey(symbol.ref, symbol.position.path)));
|
|
38
31
|
}
|
|
32
|
+
/** The key of the declaration a finding is about, or of the one holding the part it is about. */
|
|
33
|
+
function findingKey(finding) {
|
|
34
|
+
return declarationKey(finding.symbol.holder ?? finding.symbol.ref, finding.position.path);
|
|
35
|
+
}
|
|
39
36
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
37
|
+
* Every emitter's findings over one sweep, and those of any further emitter, completed by
|
|
38
|
+
* one step, but none about a declaration that falls with a write-only subject: that
|
|
39
|
+
* subject's finding names it, so no finding about it exists for a record to match.
|
|
43
40
|
*/
|
|
44
|
-
function markupKeys(input) {
|
|
45
|
-
const { swept } = input;
|
|
46
|
-
if (swept.componentFiles.length === 0) {
|
|
47
|
-
return new Set();
|
|
48
|
-
}
|
|
49
|
-
const live = new Set(liveFilesAt(swept.matrix, swept.sweep, new Set(swept.componentFiles)).map((symbol) => symbol.id));
|
|
50
|
-
return new Set(swept.matrix.union.symbols
|
|
51
|
-
.filter((symbol) => live.has(symbol.parent))
|
|
52
|
-
.map((symbol) => declarationKey(symbol.ref, symbol.position.path)));
|
|
53
|
-
}
|
|
54
|
-
/** Every emitter's findings over one sweep, and those of any further emitter, completed by one step. */
|
|
55
41
|
function completedOver(input, ...more) {
|
|
56
|
-
|
|
42
|
+
const falls = keysOf(input, fallingWithWrites(input));
|
|
43
|
+
return completed(input, [...EMITTERS.values(), ...more].flatMap((emit) => emit(input))).filter((finding) => !falls.has(findingKey(finding)));
|
|
57
44
|
}
|
|
58
45
|
/**
|
|
59
46
|
* The dials over the findings a record could name, each read against the sweep that
|
|
@@ -96,13 +83,15 @@ function atPossible(input) {
|
|
|
96
83
|
*/
|
|
97
84
|
function dialledOver(input, suppressed, found, unmarked) {
|
|
98
85
|
const would = [...unmarked, ...found];
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
86
|
+
const { sweep } = input.swept;
|
|
87
|
+
// A hold is a gap of the analysis, which leaves a record it touches dormant; a live
|
|
88
|
+
// component file's markup is no gap, so a record on one of its bindings stays in effect.
|
|
89
|
+
const unanswered = keysOf(input, sweep.held);
|
|
90
|
+
const markup = keysOf(input, sweep.bindings);
|
|
91
|
+
const heldIds = new Set(sweep.held);
|
|
92
|
+
const held = (finding) => unanswered.has(findingKey(finding)) || insideSkipped(input.swept.skipped, finding.position);
|
|
103
93
|
const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked), { withholds: held, holdsRecord: (record) => heldIds.has(record.bound) });
|
|
104
|
-
const
|
|
105
|
-
const candidates = found.filter((finding) => !ledger.withheld(finding) && !held(finding) && !markup.has(keyOf(finding)));
|
|
94
|
+
const candidates = found.filter((finding) => !ledger.withheld(finding) && !held(finding) && !markup.has(findingKey(finding)));
|
|
106
95
|
const kept = reportable(input, candidates);
|
|
107
96
|
const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
|
|
108
97
|
return {
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { isClassDeclaration, isHeritageClause, isInterfaceDeclaration, isMethodDeclaration, SyntaxKind, } from "@typescript/native/ast";
|
|
9
9
|
import { SymbolFlags } from "@typescript/native/sync";
|
|
10
|
-
import { aliasChains } from "
|
|
11
|
-
import { nodeKey } from "
|
|
12
|
-
import { must, Unanswerable } from "
|
|
10
|
+
import { aliasChains } from "../alias-chain.js";
|
|
11
|
+
import { nodeKey } from "../inventory.js";
|
|
12
|
+
import { must, Unanswerable } from "../query.js";
|
|
13
13
|
/** The interfaces, the classes and the `implements` entries of one project's own files. */
|
|
14
14
|
function walk(project, held) {
|
|
15
15
|
const interfaces = [];
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* links the read to a declaration, so only a text match can tell which member it reads.
|
|
5
5
|
*/
|
|
6
6
|
import { isBinaryExpression, isNoSubstitutionTemplateLiteral, isStringLiteral, SyntaxKind, } from "@typescript/native/ast";
|
|
7
|
-
import { sourceFilesOf } from "
|
|
8
|
-
import { bytewise } from "
|
|
7
|
+
import { sourceFilesOf } from "../source-files.js";
|
|
8
|
+
import { bytewise } from "../text/compare.js";
|
|
9
9
|
/**
|
|
10
10
|
* The text of each string literal, or template literal with no substitution, written as
|
|
11
11
|
* the left operand of an `in` expression in a file of the project that is no test file.
|
|
@@ -2,7 +2,7 @@ import { CODE } from "../contract/contract.generated.js";
|
|
|
2
2
|
import { KINDS } from "../kinds.js";
|
|
3
3
|
import { parameterRef } from "../ref.js";
|
|
4
4
|
import {} from "../intra-function-parts.js";
|
|
5
|
-
import { reachedWithTheTests } from "../sweep.js";
|
|
5
|
+
import { reachedWithTheTests } from "../liveness/sweep.js";
|
|
6
6
|
/** A list of rules as a sentence names them. */
|
|
7
7
|
function spelled(rules) {
|
|
8
8
|
if (rules.length < 2) {
|
|
@@ -84,8 +84,8 @@ export const intraFunction = (input) => {
|
|
|
84
84
|
const union = input.swept.matrix.union;
|
|
85
85
|
const byId = new Map(union.symbols.map((symbol) => [symbol.id, symbol]));
|
|
86
86
|
const reached = reachedWithTheTests(union, [
|
|
87
|
-
...input.swept.exempt.map((record) => record.id),
|
|
88
|
-
...input.swept.
|
|
87
|
+
...input.swept.sweep.exempt.map((record) => record.id),
|
|
88
|
+
...input.swept.sweep.held,
|
|
89
89
|
]);
|
|
90
90
|
const testsOfDeadCode = new Set(input.swept.sweep.candidates.filter((one) => one.testOfDeadCode).map((one) => one.id));
|
|
91
91
|
const judged = (id) => {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { CODE } from "../contract/contract.generated.js";
|
|
2
|
+
import { WORDS } from "../inventory.js";
|
|
2
3
|
import { positionKey } from "../position.js";
|
|
3
4
|
import { declarationKey } from "../ref.js";
|
|
5
|
+
import { reachabilityClasses } from "./completion.js";
|
|
4
6
|
import { bytewise } from "../text/compare.js";
|
|
5
7
|
/** The containers whose type parameters `DS1006` reports: a function, a class method and a type alias. */
|
|
6
8
|
const SIGNATURES = new Set(["function", "method", "type"]);
|
|
@@ -70,14 +72,16 @@ function parametersOnlyNamedBy(run, namedBy, member) {
|
|
|
70
72
|
.map((symbol) => symbol.name);
|
|
71
73
|
}
|
|
72
74
|
/** What one write-only finding says, the declarations its deletion takes with it included. */
|
|
73
|
-
function writeOnlyMessage(run,
|
|
75
|
+
function writeOnlyMessage(run, written, subject) {
|
|
76
|
+
const { symbol, held } = subject;
|
|
74
77
|
const writes = held.writes.length === 1 ? "once" : `at ${String(held.writes.length)} positions`;
|
|
75
78
|
const read = run.input.stores.readInTests.has(symbol.id)
|
|
76
79
|
? "read only from test files"
|
|
77
80
|
: "never read";
|
|
78
81
|
const cascade = [
|
|
79
|
-
...parametersOnlyNamedBy(run, namedBy, symbol).map((name) => `type parameter ${name}`),
|
|
82
|
+
...parametersOnlyNamedBy(run, written.namedBy, symbol).map((name) => `type parameter ${name}`),
|
|
80
83
|
...[...held.alone].sort(bytewise).map((name) => `import ${name}`),
|
|
84
|
+
...(written.named.get(symbol.id) ?? []),
|
|
81
85
|
];
|
|
82
86
|
const word = SUBJECT_WORDS[symbol.kind] ?? "member";
|
|
83
87
|
const message = `${word} ${symbol.name} is written ${writes} and ${read}`;
|
|
@@ -86,16 +90,34 @@ function writeOnlyMessage(run, namedBy, symbol, held) {
|
|
|
86
90
|
: `${message}, and deleting it with its writes deletes ${cascade.join(" and ")} too`;
|
|
87
91
|
}
|
|
88
92
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
93
|
+
* The kinds of declaration that fall with a write-only subject: a declaration of its own.
|
|
94
|
+
* A member falls with its container, and a type parameter is the type-parameter clause's.
|
|
95
|
+
*/
|
|
96
|
+
const FALLING = new Set([
|
|
97
|
+
"function",
|
|
98
|
+
"class",
|
|
99
|
+
"interface",
|
|
100
|
+
"type",
|
|
101
|
+
"enum",
|
|
102
|
+
"namespace",
|
|
103
|
+
"variable",
|
|
104
|
+
]);
|
|
105
|
+
const WRITTEN = new WeakMap();
|
|
106
|
+
/**
|
|
107
|
+
* `DS1301`'s subjects: a module-level variable, a type member or a class member the
|
|
108
|
+
* counted references store into and never read.
|
|
91
109
|
*
|
|
92
110
|
* The subject is a live declaration: a store is a reference, so the sweep holds a
|
|
93
111
|
* written one live and its candidates are never this kind's. Every reference that is
|
|
94
112
|
* not a store reads, and so does an exemption record naming the declaration.
|
|
95
113
|
*/
|
|
96
|
-
function
|
|
114
|
+
function writtenOf(run) {
|
|
115
|
+
const known = WRITTEN.get(run.input);
|
|
116
|
+
if (known !== undefined) {
|
|
117
|
+
return known;
|
|
118
|
+
}
|
|
97
119
|
const { stores } = run.input;
|
|
98
|
-
const { matrix } = run.input.swept;
|
|
120
|
+
const { matrix, sweep } = run.input.swept;
|
|
99
121
|
const uses = new Map();
|
|
100
122
|
for (const reference of stores.references) {
|
|
101
123
|
let held = uses.get(reference.to);
|
|
@@ -124,22 +146,49 @@ function writeOnlySymbols(run) {
|
|
|
124
146
|
namedBy.set(reference.to, (namedBy.get(reference.to) ?? new Set()).add(reference.from));
|
|
125
147
|
}
|
|
126
148
|
}
|
|
127
|
-
const
|
|
128
|
-
for (const symbol of matrix.union.symbols) {
|
|
149
|
+
const subjects = matrix.union.symbols.flatMap((symbol) => {
|
|
129
150
|
const held = uses.get(symbol.id);
|
|
130
|
-
|
|
151
|
+
return held === undefined ||
|
|
131
152
|
held.writes.length === 0 ||
|
|
132
153
|
held.reads > 0 ||
|
|
133
154
|
!writable(run, symbol) ||
|
|
134
155
|
stores.exempt.has(symbol.id) ||
|
|
135
|
-
run.candidates.has(symbol.id)
|
|
156
|
+
run.candidates.has(symbol.id)
|
|
157
|
+
? []
|
|
158
|
+
: [{ symbol, held }];
|
|
159
|
+
});
|
|
160
|
+
const subjectIds = new Set(subjects.map((subject) => subject.symbol.id));
|
|
161
|
+
const classOf = reachabilityClasses(run.input);
|
|
162
|
+
const named = new Map();
|
|
163
|
+
const falls = new Set();
|
|
164
|
+
for (const symbol of matrix.union.symbols) {
|
|
165
|
+
const chains = sweep.carried.get(symbol.id);
|
|
166
|
+
if (chains === undefined ||
|
|
167
|
+
subjectIds.has(symbol.id) ||
|
|
168
|
+
!FALLING.has(symbol.kind) ||
|
|
169
|
+
classOf(symbol.id) !== "certain") {
|
|
136
170
|
continue;
|
|
137
171
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
172
|
+
const innermost = chains.map((chain) => chain.find((id) => subjectIds.has(id)));
|
|
173
|
+
if (innermost.includes(undefined)) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
falls.add(symbol.id);
|
|
177
|
+
for (const subject of new Set(innermost)) {
|
|
178
|
+
const name = `${WORDS[symbol.kind]} ${symbol.name}`;
|
|
179
|
+
named.set(subject ?? "", [...(named.get(subject ?? "") ?? []), name]);
|
|
180
|
+
}
|
|
141
181
|
}
|
|
142
|
-
|
|
182
|
+
const written = { subjects, namedBy, named, falls };
|
|
183
|
+
WRITTEN.set(run.input, written);
|
|
184
|
+
return written;
|
|
185
|
+
}
|
|
186
|
+
/** `DS1301`: each subject, named with every position written and what falls with it. */
|
|
187
|
+
function writeOnlySymbols(run) {
|
|
188
|
+
const written = writtenOf(run);
|
|
189
|
+
return written.subjects.map((subject) => findingAbout(subject.symbol, CODE.writeOnlySymbol, writeOnlyMessage(run, written, subject), {
|
|
190
|
+
writePositions: subject.held.writes,
|
|
191
|
+
}));
|
|
143
192
|
}
|
|
144
193
|
/**
|
|
145
194
|
* The candidates no reference of any file names whose container is not itself dead, of
|
|
@@ -184,7 +233,14 @@ function unusedTypeParameters(run) {
|
|
|
184
233
|
* supersedes narrowing it.
|
|
185
234
|
*/
|
|
186
235
|
export function writeOnlyDeclarations(input) {
|
|
187
|
-
return new Set(
|
|
236
|
+
return new Set(writtenOf(indexed(input)).subjects.map(({ symbol }) => declarationKey(symbol.ref, symbol.position.path)));
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* The declarations that fall with a write-only subject, whatever the severity: a finding
|
|
240
|
+
* about one would name code its subject's deletion already deletes.
|
|
241
|
+
*/
|
|
242
|
+
export function fallingWithWrites(input) {
|
|
243
|
+
return writtenOf(indexed(input)).falls;
|
|
188
244
|
}
|
|
189
245
|
/**
|
|
190
246
|
* The findings of the reads-and-writes kinds, `DS1301`, `DS1005` and `DS1006`, in that
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CODE } from "../contract/contract.generated.js";
|
|
2
|
-
import {
|
|
2
|
+
import { WORDS } from "../inventory.js";
|
|
3
3
|
import { unreachableExports } from "./visibility-narrowing.js";
|
|
4
4
|
/** The kinds of declaration that are members of a class, an interface, a type or an enum. */
|
|
5
5
|
const MEMBERS = new Set([
|
|
@@ -19,24 +19,6 @@ const NOT_REPORTED_HERE = new Set([
|
|
|
19
19
|
"interface-method",
|
|
20
20
|
"type-parameter",
|
|
21
21
|
]);
|
|
22
|
-
/** The reader's word for each kind of declaration a finding of this emitter names. */
|
|
23
|
-
const WORDS = {
|
|
24
|
-
file: "file",
|
|
25
|
-
function: "function",
|
|
26
|
-
class: "class",
|
|
27
|
-
interface: "interface",
|
|
28
|
-
type: "type alias",
|
|
29
|
-
enum: "enum",
|
|
30
|
-
namespace: "namespace",
|
|
31
|
-
variable: "variable",
|
|
32
|
-
"export-alias": "re-export",
|
|
33
|
-
method: "method",
|
|
34
|
-
"class-member": "class member",
|
|
35
|
-
"interface-method": "interface method",
|
|
36
|
-
"type-member": "type member",
|
|
37
|
-
"enum-member": "enum member",
|
|
38
|
-
"type-parameter": "type parameter",
|
|
39
|
-
};
|
|
40
22
|
/** What the test-of-dead-code kind says, which states the rule it applies. */
|
|
41
23
|
const TEST_OF_DEAD_CODE = "every target symbol this test references is reported dead";
|
|
42
24
|
/**
|
|
@@ -50,7 +32,7 @@ const TEST_OF_DEAD_CODE = "every target symbol this test references is reported
|
|
|
50
32
|
function codeOf(judged) {
|
|
51
33
|
const { candidate, symbol } = judged;
|
|
52
34
|
if (candidate.testOfDeadCode) {
|
|
53
|
-
return
|
|
35
|
+
return CODE.testOfDeadCode;
|
|
54
36
|
}
|
|
55
37
|
if ((judged.test && !candidate.unreferencedTest) || NOT_REPORTED_HERE.has(symbol.kind)) {
|
|
56
38
|
return undefined;
|
|
@@ -201,7 +183,6 @@ function emitterFindings(input, fallen) {
|
|
|
201
183
|
const union = input.swept.matrix.union;
|
|
202
184
|
const dead = new Set(input.swept.sweep.candidates.map((candidate) => candidate.id));
|
|
203
185
|
const unreachable = unreachableExports(input);
|
|
204
|
-
const unseenAPI = testsUnseenAPI(input);
|
|
205
186
|
let brands;
|
|
206
187
|
const found = [];
|
|
207
188
|
for (const candidate of input.swept.sweep.candidates) {
|
|
@@ -220,7 +201,6 @@ function emitterFindings(input, fallen) {
|
|
|
220
201
|
test: union.test[at] === true,
|
|
221
202
|
deprecated: input.deprecated.has(symbol.id),
|
|
222
203
|
unreachable: unreachable.has(symbol.id),
|
|
223
|
-
testsUnseenAPI: candidate.testOfDeadCode && unseenAPI(candidate.id),
|
|
224
204
|
};
|
|
225
205
|
const code = codeOf(judged);
|
|
226
206
|
if (code === undefined ||
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { CODE } from "../contract/contract.generated.js";
|
|
2
|
-
import { OUTSIDE } from "../graph.js";
|
|
2
|
+
import { OUTSIDE } from "../liveness/graph.js";
|
|
3
|
+
import { WORDS } from "../inventory.js";
|
|
3
4
|
import { declarationKey } from "../ref.js";
|
|
4
|
-
import { runnerCalls } from "../sweep.js";
|
|
5
|
+
import { runnerCalls } from "../liveness/sweep.js";
|
|
6
|
+
import { exposedTypes, exposes } from "../exposure.js";
|
|
5
7
|
import { writeOnlyDeclarations } from "./reads-and-writes.js";
|
|
6
8
|
/**
|
|
7
9
|
* The root kinds by which a consumer importing the package reaches a declaration. A
|
|
@@ -9,17 +11,6 @@ import { writeOnlyDeclarations } from "./reads-and-writes.js";
|
|
|
9
11
|
* from outside the reference graph: a runtime, a tool, a framework or the configuration.
|
|
10
12
|
*/
|
|
11
13
|
const PUBLISHING = new Set(["manifest-entry", "published-api"]);
|
|
12
|
-
/** The reader's word for each kind of declaration this emitter reports. */
|
|
13
|
-
const WORDS = {
|
|
14
|
-
function: "function",
|
|
15
|
-
class: "class",
|
|
16
|
-
interface: "interface",
|
|
17
|
-
type: "type alias",
|
|
18
|
-
enum: "enum",
|
|
19
|
-
namespace: "namespace",
|
|
20
|
-
variable: "variable",
|
|
21
|
-
"export-alias": "re-export",
|
|
22
|
-
};
|
|
23
14
|
function publishes(kinds) {
|
|
24
15
|
return [...(kinds ?? [])].some((kind) => PUBLISHING.has(kind));
|
|
25
16
|
}
|
|
@@ -102,9 +93,6 @@ function topLevelExport(facts, at, symbol) {
|
|
|
102
93
|
const parent = facts.graph.parent[at] ?? OUTSIDE;
|
|
103
94
|
return symbol.exported && parent !== OUTSIDE && facts.graph.symbols[parent]?.kind === "file";
|
|
104
95
|
}
|
|
105
|
-
function wordOf(symbol) {
|
|
106
|
-
return WORDS[symbol.kind] ?? symbol.kind;
|
|
107
|
-
}
|
|
108
96
|
/** The visibility each narrowing code names, which only the narrowing codes carry. */
|
|
109
97
|
const NARROWER = {
|
|
110
98
|
[CODE.redundantExportKeyword]: "file",
|
|
@@ -145,7 +133,7 @@ function narrowing(facts, at, symbol, world) {
|
|
|
145
133
|
(facts.named[at] ?? 0) === 0) {
|
|
146
134
|
return undefined;
|
|
147
135
|
}
|
|
148
|
-
const word =
|
|
136
|
+
const word = WORDS[symbol.kind];
|
|
149
137
|
switch (facts.reach[at]) {
|
|
150
138
|
case "file":
|
|
151
139
|
if (facts.published[file] === true && !closed) {
|
|
@@ -153,7 +141,7 @@ function narrowing(facts, at, symbol, world) {
|
|
|
153
141
|
}
|
|
154
142
|
return findingOf(symbol, CODE.redundantExportKeyword, `exported ${word} is referenced only inside the file that declares it`);
|
|
155
143
|
case "package":
|
|
156
|
-
if (!closed || !publishes(facts.rootedBy[at])) {
|
|
144
|
+
if (!closed || !publishes(facts.rootedBy[at]) || exposes(world.exposed, symbol)) {
|
|
157
145
|
return undefined;
|
|
158
146
|
}
|
|
159
147
|
return findingOf(symbol, CODE.unnecessaryExport, `exported ${word} is referenced only inside the package that declares it`);
|
|
@@ -181,7 +169,7 @@ function unreachable(facts, at, symbol, candidate, input) {
|
|
|
181
169
|
const found = candidate.relation === "reachability"
|
|
182
170
|
? "is referenced only from declarations that are themselves dead"
|
|
183
171
|
: "has no reference in the target";
|
|
184
|
-
return findingOf(symbol, CODE.unreachableExport, `exported ${
|
|
172
|
+
return findingOf(symbol, CODE.unreachableExport, `exported ${WORDS[symbol.kind]} ${found}, and nothing outside can import the file that declares it`);
|
|
185
173
|
}
|
|
186
174
|
/**
|
|
187
175
|
* The visibility-narrowing kinds over one run: `DS1501`, `DS1101` and `DS1503`. A finding
|
|
@@ -194,7 +182,13 @@ export function narrowings(input) {
|
|
|
194
182
|
const facts = factsOf(input.swept.matrix.union);
|
|
195
183
|
const { declared, loaded } = input.boundary.consumers;
|
|
196
184
|
const closed = input.config.consumers.complete && declared.every((id) => loaded.includes(id));
|
|
197
|
-
const world = {
|
|
185
|
+
const world = {
|
|
186
|
+
closed,
|
|
187
|
+
writeOnly: writeOnlyDeclarations(input),
|
|
188
|
+
exposed: closed
|
|
189
|
+
? exposedTypes(facts.graph, facts.reach.map((reach) => reach === "outside"), input.boundary.carriers)
|
|
190
|
+
: new Set(),
|
|
191
|
+
};
|
|
198
192
|
const candidates = new Map(input.swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
|
|
199
193
|
const generated = input.config.analysis.generated_files === "include" ? undefined : input.files.generated;
|
|
200
194
|
const findings = [];
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The framework-lifecycle class: a member a
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* through a chain, a class its `bases` name.
|
|
6
|
-
*
|
|
2
|
+
* The framework-lifecycle class: a member a lifecycle contract lists is held back on each
|
|
3
|
+
* class that is the contract's component, being a class a decorator or a call the
|
|
4
|
+
* contract's `components` name is attached to or passes, or one extending, directly or
|
|
5
|
+
* through a chain, a class its `bases` name. The contracts are the platform's custom
|
|
6
|
+
* elements and each `ts.lifecycle_contracts` entry. A component's members are its type's,
|
|
7
|
+
* so a member a base class declares answers for every subclass.
|
|
7
8
|
*/
|
|
8
9
|
import { isClassDeclaration, isClassExpression, isHeritageClause, SyntaxKind, } from "@typescript/native/ast";
|
|
9
10
|
import { aliasChains } from "./alias-chain.js";
|
|
@@ -13,6 +14,29 @@ import { entriesAt, names, namesADeclaration, resolveEntries, spelledEntry, } fr
|
|
|
13
14
|
import { nodeKey } from "./inventory.js";
|
|
14
15
|
import { renderPosition } from "./position.js";
|
|
15
16
|
import { must } from "./query.js";
|
|
17
|
+
/**
|
|
18
|
+
* The platform's own contract (references-ts.38): `CustomElementRegistry.define` reads
|
|
19
|
+
* these members off the class it registers, its constructor's and its prototype's
|
|
20
|
+
* (https://html.spec.whatwg.org/multipage/custom-elements.html#dom-customelementregistry-define),
|
|
21
|
+
* and calls each callback as a custom element reaction.
|
|
22
|
+
*/
|
|
23
|
+
const CUSTOM_ELEMENTS = {
|
|
24
|
+
components: [{ global: "CustomElementRegistry.define" }],
|
|
25
|
+
members: [
|
|
26
|
+
"observedAttributes:static",
|
|
27
|
+
"disabledFeatures:static",
|
|
28
|
+
"formAssociated:static",
|
|
29
|
+
"connectedCallback",
|
|
30
|
+
"disconnectedCallback",
|
|
31
|
+
"connectedMoveCallback",
|
|
32
|
+
"adoptedCallback",
|
|
33
|
+
"attributeChangedCallback",
|
|
34
|
+
"formAssociatedCallback",
|
|
35
|
+
"formResetCallback",
|
|
36
|
+
"formDisabledCallback",
|
|
37
|
+
"formStateRestoreCallback",
|
|
38
|
+
],
|
|
39
|
+
};
|
|
16
40
|
/** Every contract's components and bases, resolved in one pass over the entries. */
|
|
17
41
|
function resolveContracts(project, held, contracts) {
|
|
18
42
|
const entries = contracts.flatMap((contract) => [
|
|
@@ -155,16 +179,17 @@ function byBases(project, held, chains, contracts) {
|
|
|
155
179
|
}
|
|
156
180
|
/**
|
|
157
181
|
* The framework-lifecycle detector: each member a contract lists, on each class that
|
|
158
|
-
* is a component of that contract's framework, recorded where the class
|
|
159
|
-
* be one.
|
|
182
|
+
* is a component of that contract's framework, recorded once per place where the class
|
|
183
|
+
* was found to be one.
|
|
160
184
|
*/
|
|
161
185
|
export function frameworkLifecycle(input) {
|
|
162
186
|
const { project, held, targetRoot } = input;
|
|
163
|
-
|
|
187
|
+
const declared = [CUSTOM_ELEMENTS, ...input.ts.lifecycle_contracts];
|
|
188
|
+
const contracts = resolveContracts(project, held, declared);
|
|
189
|
+
if (contracts.every((one) => one.components.length === 0 && one.bases.length === 0)) {
|
|
164
190
|
return [];
|
|
165
191
|
}
|
|
166
192
|
const chains = aliasChains(project, held);
|
|
167
|
-
const contracts = resolveContracts(project, held, input.ts.lifecycle_contracts);
|
|
168
193
|
const components = [
|
|
169
194
|
...byComponents(project, held, chains, contracts),
|
|
170
195
|
...byBases(project, held, chains, contracts),
|
|
@@ -173,22 +198,24 @@ export function frameworkLifecycle(input) {
|
|
|
173
198
|
return [];
|
|
174
199
|
}
|
|
175
200
|
const ids = new Set(components.map((one) => one.id));
|
|
176
|
-
const sides =
|
|
201
|
+
const sides = declared.some((contract) => contract.members.some((name) => name.endsWith(":static")))
|
|
177
202
|
? ["instance", "static"]
|
|
178
203
|
: ["instance"];
|
|
179
204
|
const members = sides.map((side) => classMembers(project, held, ids, side));
|
|
180
205
|
const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
|
|
181
|
-
const found =
|
|
206
|
+
const found = new Map();
|
|
182
207
|
for (const component of components) {
|
|
183
208
|
const listed = new Set(component.contract.members);
|
|
184
209
|
const site = renderPosition(component.site.getSourceFile(), targetRoot, component.site.getStart());
|
|
185
210
|
for (const member of members.flatMap((side) => side.get(component.id) ?? [])) {
|
|
186
211
|
const symbol = byId.get(member.id);
|
|
187
212
|
const spelled = symbol === undefined ? undefined : memberComponent(symbol, byId);
|
|
188
|
-
|
|
189
|
-
|
|
213
|
+
// A configured contract may name the platform's registry too; one record answers both.
|
|
214
|
+
const key = `${member.id}\0${component.detail}\0${site.path}:${String(site.line)}:${String(site.column)}`;
|
|
215
|
+
if (spelled !== undefined && listed.has(spelled) && !found.has(key)) {
|
|
216
|
+
found.set(key, { id: member.id, detail: component.detail, site });
|
|
190
217
|
}
|
|
191
218
|
}
|
|
192
219
|
}
|
|
193
|
-
return found;
|
|
220
|
+
return [...found.values()];
|
|
194
221
|
}
|