@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
|
@@ -15,50 +15,40 @@ const RANK = { certain: 3, probable: 2, possible: 1 };
|
|
|
15
15
|
function capped(found, ceiling) {
|
|
16
16
|
return RANK[found] <= RANK[ceiling] ? found : ceiling;
|
|
17
17
|
}
|
|
18
|
-
/** The kind whose default turns on what the run knows about the target's consumers. */
|
|
19
|
-
const UNUSED_EXPORTED = "DS1001";
|
|
20
18
|
/** Whether the run loaded every consumer the scope declared, which holds of a scope declaring none. */
|
|
21
19
|
function everyDeclaredLoaded(boundary) {
|
|
22
20
|
const { declared, loaded } = boundary.consumers;
|
|
23
21
|
return declared.every((id) => loaded.includes(id));
|
|
24
22
|
}
|
|
25
|
-
/**
|
|
26
|
-
* Whether a library's published API may have a caller the run cannot see: the target is
|
|
27
|
-
* a library, and the run either holds no consumer information or did not load a consumer
|
|
28
|
-
* the scope declared. A configuration declaring the consumer set complete holds consumer
|
|
29
|
-
* information whether or not it declares a consumer, because it says no other exists.
|
|
30
|
-
*/
|
|
31
|
-
function openWorld(input) {
|
|
32
|
-
const known = input.config.consumersComplete || input.boundary.consumers.declared.length > 0;
|
|
33
|
-
return input.config.targetKind === "library" && !(known && everyDeclaredLoaded(input.boundary));
|
|
34
|
-
}
|
|
35
23
|
/**
|
|
36
24
|
* The severity the configuration gives one code: the key naming the code, else the key
|
|
37
|
-
* naming its family, else the code's default.
|
|
38
|
-
* `allow` while a library's published API is open to callers the run cannot see.
|
|
25
|
+
* naming its family, else the code's default.
|
|
39
26
|
*/
|
|
40
27
|
function severityOf(input, code, fallback) {
|
|
41
28
|
const { config } = input;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
29
|
+
return (config.severity.get(code) ?? config.severity.get(code.slice(0, FAMILY_KEY_LENGTH)) ?? fallback);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The declarations the roots name as a library's published API, each public member of a
|
|
33
|
+
* published type included.
|
|
34
|
+
*/
|
|
35
|
+
function publishedIds(union) {
|
|
36
|
+
return new Set(union.rooted
|
|
37
|
+
.filter((root) => root.kind === "published-api")
|
|
38
|
+
.flatMap((root) => union.symbols[root.at]?.id ?? []));
|
|
47
39
|
}
|
|
48
40
|
/**
|
|
49
41
|
* The reachability class of one declaration of the run. A declaration of a library's
|
|
50
|
-
* published API
|
|
51
|
-
*
|
|
52
|
-
* declared
|
|
53
|
-
*
|
|
54
|
-
* Every other declaration has every reference
|
|
55
|
-
* and a `#private` name among them, and is
|
|
42
|
+
* published API, a public member of a published type included, has callers outside the
|
|
43
|
+
* target: it is `certain` where the run loaded every consumer the scope declared and
|
|
44
|
+
* declared at least one, `probable` where it declared consumers and did not load them
|
|
45
|
+
* all, and `possible` where it declared none. Whether the configuration declares the
|
|
46
|
+
* consumer set complete decides nothing here. Every other declaration has every reference
|
|
47
|
+
* in the loaded program, a private member and a `#private` name among them, and is
|
|
48
|
+
* `certain`.
|
|
56
49
|
*/
|
|
57
50
|
function reachabilityClasses(input) {
|
|
58
|
-
const
|
|
59
|
-
const published = new Set(union.rooted
|
|
60
|
-
.filter((root) => root.kind === "published-api")
|
|
61
|
-
.map((root) => union.symbols[root.at]?.id));
|
|
51
|
+
const published = publishedIds(input.swept.matrix.union);
|
|
62
52
|
const { declared } = input.boundary.consumers;
|
|
63
53
|
const outside = declared.length === 0
|
|
64
54
|
? "possible"
|
|
@@ -126,7 +116,8 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
126
116
|
const classOf = reachabilityClasses(input);
|
|
127
117
|
const componentOf = findingComponents(swept, input.config.reporters.cascade);
|
|
128
118
|
let minted = swept.sweep.components.length;
|
|
129
|
-
|
|
119
|
+
const dead = new Set(swept.sweep.components.map((component) => component.id));
|
|
120
|
+
const done = findings.map((finding) => {
|
|
130
121
|
const row = kinds.get(finding.code);
|
|
131
122
|
if (row === undefined) {
|
|
132
123
|
throw new Error(`${finding.code} names no live row of the issue-kind vocabulary`);
|
|
@@ -161,6 +152,7 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
161
152
|
? union.made[at]
|
|
162
153
|
: { production: candidate.productionRefs, test: candidate.testRefs };
|
|
163
154
|
const reachabilityClass = judged === undefined ? "certain" : classOf(judged.id);
|
|
155
|
+
const generated = input.files.generated?.has(finding.position.path) === true;
|
|
164
156
|
return {
|
|
165
157
|
code: finding.code,
|
|
166
158
|
kind: row.name,
|
|
@@ -171,7 +163,7 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
171
163
|
confidence: capped(reachabilityClass, row.maxClass),
|
|
172
164
|
...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
|
|
173
165
|
testOnly: judged !== undefined && counts?.production === 0 && counts.test > 0,
|
|
174
|
-
generated
|
|
166
|
+
generated,
|
|
175
167
|
component,
|
|
176
168
|
retainedBy: [],
|
|
177
169
|
configurations: candidate?.configurations ??
|
|
@@ -179,12 +171,34 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
179
171
|
? swept.matrix.configurations
|
|
180
172
|
: (swept.matrix.heldIn[at] ?? swept.matrix.configurations)),
|
|
181
173
|
consumersLoaded: input.boundary.consumers.loaded,
|
|
182
|
-
fixability: row.fixability,
|
|
174
|
+
fixability: generated ? "none" : row.fixability,
|
|
183
175
|
severity: severityOf(input, finding.code, row.defaultSeverity),
|
|
184
176
|
message: finding.message,
|
|
185
177
|
details: finding.details ?? {},
|
|
186
178
|
};
|
|
187
179
|
});
|
|
180
|
+
return componentCapped(done, dead);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* The findings with every finding of a dead component capped by the lowest confidence
|
|
184
|
+
* among the component's root members, so the minimum confidence reports or withholds a
|
|
185
|
+
* component whole.
|
|
186
|
+
*/
|
|
187
|
+
function componentCapped(findings, dead) {
|
|
188
|
+
const lowest = new Map();
|
|
189
|
+
for (const finding of findings) {
|
|
190
|
+
const { id, root } = finding.component;
|
|
191
|
+
if (root && dead.has(id)) {
|
|
192
|
+
const held = lowest.get(id);
|
|
193
|
+
lowest.set(id, held === undefined ? finding.confidence : capped(held, finding.confidence));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return findings.map((finding) => {
|
|
197
|
+
const cap = lowest.get(finding.component.id);
|
|
198
|
+
return cap === undefined || RANK[cap] >= RANK[finding.confidence]
|
|
199
|
+
? finding
|
|
200
|
+
: { ...finding, confidence: cap };
|
|
201
|
+
});
|
|
188
202
|
}
|
|
189
203
|
/** Whether the dials withhold a finding by its own severity or confidence. */
|
|
190
204
|
function dialed(finding, minConfidence) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { evaluateEdges } from "../edges.js";
|
|
2
|
+
import { liveFilesAt } from "../matrix.js";
|
|
2
3
|
import { ledgerOf } from "../suppress.js";
|
|
4
|
+
import { insideSkipped } from "../type-errors.js";
|
|
3
5
|
import { completed, dialsOf, reportable } from "./completion.js";
|
|
4
6
|
import { dependenciesAndModuleMachinery } from "./dependencies-and-module-machinery.js";
|
|
5
7
|
import { interfaces } from "./interfaces.js";
|
|
@@ -33,6 +35,21 @@ function heldRefs(input) {
|
|
|
33
35
|
.filter((symbol) => held.has(symbol.id))
|
|
34
36
|
.map((symbol) => symbol.ref));
|
|
35
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* The references of every top-level declaration of a live component file. The file's
|
|
40
|
+
* markup may use each of them, which no analysis of the markup can rule out, so the run
|
|
41
|
+
* reports nothing about one while the file is live.
|
|
42
|
+
*/
|
|
43
|
+
function markupRefs(input) {
|
|
44
|
+
const { swept } = input;
|
|
45
|
+
if (swept.componentFiles.length === 0) {
|
|
46
|
+
return new Set();
|
|
47
|
+
}
|
|
48
|
+
const live = new Set(liveFilesAt(swept.matrix, swept.sweep, new Set(swept.componentFiles)).map((symbol) => symbol.id));
|
|
49
|
+
return new Set(swept.matrix.union.symbols
|
|
50
|
+
.filter((symbol) => live.has(symbol.parent))
|
|
51
|
+
.map((symbol) => symbol.ref));
|
|
52
|
+
}
|
|
36
53
|
/** Every family's findings over one sweep, and those of any further emitter, completed by one step. */
|
|
37
54
|
function completedOver(input, ...more) {
|
|
38
55
|
return completed(input, [...EMITTERS.values(), ...more].flatMap((emit) => emit(input)));
|
|
@@ -64,7 +81,11 @@ export function decidedFindings(input, suppressed = { unmarked: input, records:
|
|
|
64
81
|
const would = [...unmarked, ...found];
|
|
65
82
|
const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
|
|
66
83
|
const unanswered = heldRefs(input);
|
|
67
|
-
const
|
|
84
|
+
const markup = markupRefs(input);
|
|
85
|
+
const kept = reportable(input, found.filter((finding) => !ledger.withheld(finding) &&
|
|
86
|
+
!unanswered.has(finding.symbol.ref) &&
|
|
87
|
+
!markup.has(finding.symbol.ref) &&
|
|
88
|
+
!insideSkipped(input.swept.skipped, finding.position)));
|
|
68
89
|
const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
|
|
69
90
|
return {
|
|
70
91
|
findings: evaluated.findings,
|
|
@@ -26,6 +26,7 @@ function indexed(input) {
|
|
|
26
26
|
return {
|
|
27
27
|
symbolOf,
|
|
28
28
|
inTest: (id) => union.test[union.at(id)] === true,
|
|
29
|
+
inSupport: (id) => union.support[union.at(id)] === true,
|
|
29
30
|
candidateOf: (id) => candidates.get(id),
|
|
30
31
|
implementationsOf: (id) => [...(input.implementations.classes.get(id) ?? [])]
|
|
31
32
|
.map(symbolOf)
|
|
@@ -55,14 +56,17 @@ function findingOf(code, symbol, message, implementations) {
|
|
|
55
56
|
* interface, whose members fall with it unreported; and `DS1203`, a method of a live
|
|
56
57
|
* interface no reference names, unless it is a marker, a method whose every
|
|
57
58
|
* implementation has an empty body. A test file's declaration is neither kind's
|
|
58
|
-
* subject, because a production sweep counts none of the references it can have
|
|
59
|
+
* subject, because a production sweep counts none of the references it can have, and
|
|
60
|
+
* neither is a test-support declaration test code references, which that code keeps live.
|
|
59
61
|
*/
|
|
60
62
|
export const interfaces = (input) => {
|
|
61
63
|
const run = indexed(input);
|
|
62
64
|
const found = [];
|
|
63
65
|
for (const candidate of input.swept.sweep.candidates) {
|
|
64
66
|
const symbol = run.symbolOf(candidate.id);
|
|
65
|
-
if (symbol === undefined ||
|
|
67
|
+
if (symbol === undefined ||
|
|
68
|
+
run.inTest(candidate.id) ||
|
|
69
|
+
(run.inSupport(candidate.id) && candidate.testRefs > 0)) {
|
|
66
70
|
continue;
|
|
67
71
|
}
|
|
68
72
|
if (symbol.kind === "interface") {
|
|
@@ -36,18 +36,55 @@ function findingAbout(symbol, code, message, details) {
|
|
|
36
36
|
}
|
|
37
37
|
/**
|
|
38
38
|
* Whether one declaration is a subject of the write-only kind: a module-level variable,
|
|
39
|
-
* or a class member that is state rather than code. A getter
|
|
40
|
-
* store into one runs it.
|
|
39
|
+
* a member of an object type, or a class member that is state rather than code. A getter
|
|
40
|
+
* or a setter is code, so a store into one runs it.
|
|
41
41
|
*/
|
|
42
42
|
function writable(run, symbol) {
|
|
43
|
-
if (symbol.kind === "variable") {
|
|
43
|
+
if (symbol.kind === "variable" || symbol.kind === "type-member") {
|
|
44
44
|
return true;
|
|
45
45
|
}
|
|
46
46
|
return symbol.kind === "class-member" && !run.input.stores.accessors.has(symbol.id);
|
|
47
47
|
}
|
|
48
|
+
/** The reader's word for each kind of the write-only kind's subject. */
|
|
49
|
+
const SUBJECT_WORDS = {
|
|
50
|
+
variable: "variable",
|
|
51
|
+
"class-member": "member",
|
|
52
|
+
"type-member": "type member",
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* The type parameters of one member's declaring type that only the member names, which
|
|
56
|
+
* deleting the member leaves unused.
|
|
57
|
+
*/
|
|
58
|
+
function parametersOnlyNamedBy(run, namedBy, member) {
|
|
59
|
+
return [...run.symbols.values()]
|
|
60
|
+
.filter((symbol) => {
|
|
61
|
+
const from = namedBy.get(symbol.id);
|
|
62
|
+
return (symbol.kind === "type-parameter" &&
|
|
63
|
+
symbol.parent === member.parent &&
|
|
64
|
+
from?.size === 1 &&
|
|
65
|
+
from.has(member.id));
|
|
66
|
+
})
|
|
67
|
+
.map((symbol) => symbol.name);
|
|
68
|
+
}
|
|
69
|
+
/** What one write-only finding says, the declarations its deletion takes with it included. */
|
|
70
|
+
function writeOnlyMessage(run, namedBy, symbol, held) {
|
|
71
|
+
const writes = held.writes.length === 1 ? "once" : `at ${String(held.writes.length)} positions`;
|
|
72
|
+
const read = run.input.stores.readInTests.has(symbol.id)
|
|
73
|
+
? "read only from test files"
|
|
74
|
+
: "never read";
|
|
75
|
+
const cascade = [
|
|
76
|
+
...parametersOnlyNamedBy(run, namedBy, symbol).map((name) => `type parameter ${name}`),
|
|
77
|
+
...[...held.alone].sort().map((name) => `import ${name}`),
|
|
78
|
+
];
|
|
79
|
+
const word = SUBJECT_WORDS[symbol.kind] ?? "member";
|
|
80
|
+
const message = `${word} ${symbol.name} is written ${writes} and ${read}`;
|
|
81
|
+
return cascade.length === 0
|
|
82
|
+
? message
|
|
83
|
+
: `${message}, and deleting it with its writes deletes ${cascade.join(" and ")} too`;
|
|
84
|
+
}
|
|
48
85
|
/**
|
|
49
|
-
* `DS1301`: a module-level variable or a class member the counted
|
|
50
|
-
* and never read, named with every position written.
|
|
86
|
+
* `DS1301`: a module-level variable, a type member or a class member the counted
|
|
87
|
+
* references store into and never read, named with every position written.
|
|
51
88
|
*
|
|
52
89
|
* The subject is a live declaration: a store is a reference, so the sweep holds a
|
|
53
90
|
* written one live and its candidates are never this kind's. Every reference that is
|
|
@@ -60,7 +97,7 @@ function writeOnlySymbols(run) {
|
|
|
60
97
|
for (const reference of stores.references) {
|
|
61
98
|
let held = uses.get(reference.to);
|
|
62
99
|
if (held === undefined) {
|
|
63
|
-
held = { writes: [], reads: 0 };
|
|
100
|
+
held = { writes: [], reads: 0, alone: new Set() };
|
|
64
101
|
uses.set(reference.to, held);
|
|
65
102
|
}
|
|
66
103
|
// A consumer's store is written at a position no report of the target can name, so
|
|
@@ -69,12 +106,21 @@ function writeOnlySymbols(run) {
|
|
|
69
106
|
held.reads += 1;
|
|
70
107
|
continue;
|
|
71
108
|
}
|
|
109
|
+
for (const name of reference.alone ?? []) {
|
|
110
|
+
held.alone.add(name);
|
|
111
|
+
}
|
|
72
112
|
const at = reference.position;
|
|
73
113
|
const key = positionKey(at);
|
|
74
114
|
if (!held.writes.some((written) => positionKey(written) === key)) {
|
|
75
115
|
held.writes.push({ path: at.path, line: at.line, column: at.column, endLine: at.line });
|
|
76
116
|
}
|
|
77
117
|
}
|
|
118
|
+
const namedBy = new Map();
|
|
119
|
+
for (const reference of stores.references) {
|
|
120
|
+
if (run.symbols.get(reference.to)?.kind === "type-parameter") {
|
|
121
|
+
namedBy.set(reference.to, (namedBy.get(reference.to) ?? new Set()).add(reference.from));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
78
124
|
const found = [];
|
|
79
125
|
for (const symbol of matrix.union.symbols) {
|
|
80
126
|
const held = uses.get(symbol.id);
|
|
@@ -86,9 +132,7 @@ function writeOnlySymbols(run) {
|
|
|
86
132
|
run.candidates.has(symbol.id)) {
|
|
87
133
|
continue;
|
|
88
134
|
}
|
|
89
|
-
|
|
90
|
-
const word = symbol.kind === "variable" ? "variable" : "member";
|
|
91
|
-
found.push(findingAbout(symbol, "DS1301", `${word} ${symbol.name} is written ${writes} and never read`, {
|
|
135
|
+
found.push(findingAbout(symbol, "DS1301", writeOnlyMessage(run, namedBy, symbol, held), {
|
|
92
136
|
writePositions: held.writes,
|
|
93
137
|
}));
|
|
94
138
|
}
|
|
@@ -40,8 +40,9 @@ const TEST_OF_DEAD_CODE = "every target symbol this test references is reported
|
|
|
40
40
|
/**
|
|
41
41
|
* The code one candidate is reported under, or none, each symbol once under the most
|
|
42
42
|
* specific code: a test of dead code first; no other test-file declaration, member of a
|
|
43
|
-
* dead container or enum member nothing names
|
|
44
|
-
*
|
|
43
|
+
* dead container or enum member nothing names, nor a test-support declaration test code
|
|
44
|
+
* references; then deprecation, a test reference, and the unreferenced codes, an
|
|
45
|
+
* unimportable unused export being the unreachable kind's.
|
|
45
46
|
*/
|
|
46
47
|
function codeOf(judged) {
|
|
47
48
|
const { candidate, symbol } = judged;
|
|
@@ -55,6 +56,9 @@ function codeOf(judged) {
|
|
|
55
56
|
if (symbol.kind === "enum-member" && !referenced) {
|
|
56
57
|
return undefined;
|
|
57
58
|
}
|
|
59
|
+
if (judged.support && candidate.testRefs > 0) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
58
62
|
if (candidate.productionRefs === 0 && judged.deprecated) {
|
|
59
63
|
return "DS1006";
|
|
60
64
|
}
|
|
@@ -110,6 +114,43 @@ function messageOf(code, judged) {
|
|
|
110
114
|
}
|
|
111
115
|
return `${subject} has no reference in the target`;
|
|
112
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* The members nothing writes or reads that are brands: each names a type parameter of
|
|
119
|
+
* its declaring type that no other part of the declaration names, other than another
|
|
120
|
+
* such member. They are what make two instantiations of the type distinct types, so
|
|
121
|
+
* deleting them would let one instantiation stand for another.
|
|
122
|
+
*/
|
|
123
|
+
function brandMembers(input) {
|
|
124
|
+
const union = input.swept.matrix.union;
|
|
125
|
+
const unreferenced = new Map(input.swept.sweep.candidates
|
|
126
|
+
.filter((candidate) => candidate.productionRefs + candidate.testRefs === 0)
|
|
127
|
+
.map((candidate) => [candidate.id, candidate]));
|
|
128
|
+
const namedBy = new Map();
|
|
129
|
+
for (const reference of input.stores.references) {
|
|
130
|
+
const to = union.symbols[union.at(reference.to)];
|
|
131
|
+
if (to?.kind === "type-parameter") {
|
|
132
|
+
namedBy.set(to.id, (namedBy.get(to.id) ?? new Set()).add(reference.from));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const brands = new Set();
|
|
136
|
+
for (const [parameter, from] of namedBy) {
|
|
137
|
+
const owner = union.symbols[union.at(parameter)]?.parent;
|
|
138
|
+
const members = [...from].map((id) => union.symbols[union.at(id)]);
|
|
139
|
+
if (members.every((member) => member !== undefined &&
|
|
140
|
+
member.parent === owner &&
|
|
141
|
+
BRANDS.has(member.kind) &&
|
|
142
|
+
unreferenced.has(member.id))) {
|
|
143
|
+
for (const member of members) {
|
|
144
|
+
if (member !== undefined) {
|
|
145
|
+
brands.add(member.id);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return brands;
|
|
151
|
+
}
|
|
152
|
+
/** The kinds of member a brand is: a data member of a type or of a class. */
|
|
153
|
+
const BRANDS = new Set(["type-member", "class-member"]);
|
|
113
154
|
/** The findings of the unused-declarations family, `DS1000` to `DS1099`. */
|
|
114
155
|
export const unusedDeclarations = (input) => familyFindings(input, false);
|
|
115
156
|
/**
|
|
@@ -123,6 +164,7 @@ function familyFindings(input, fallen) {
|
|
|
123
164
|
const union = input.swept.matrix.union;
|
|
124
165
|
const dead = new Set(input.swept.sweep.candidates.map((candidate) => candidate.id));
|
|
125
166
|
const unreachable = unreachableExports(input);
|
|
167
|
+
let brands;
|
|
126
168
|
const found = [];
|
|
127
169
|
for (const candidate of input.swept.sweep.candidates) {
|
|
128
170
|
const at = union.at(candidate.id);
|
|
@@ -138,12 +180,14 @@ function familyFindings(input, fallen) {
|
|
|
138
180
|
candidate,
|
|
139
181
|
symbol,
|
|
140
182
|
test: union.test[at] === true,
|
|
183
|
+
support: union.support[at] === true,
|
|
141
184
|
containerDead: containerDead && !fallen,
|
|
142
185
|
deprecated: input.deprecated.has(symbol.id),
|
|
143
186
|
unreachable: unreachable.has(symbol.id),
|
|
144
187
|
};
|
|
145
188
|
const code = codeOf(judged);
|
|
146
|
-
if (code === undefined
|
|
189
|
+
if (code === undefined ||
|
|
190
|
+
(code === "DS1003" && (brands ??= brandMembers(input)).has(symbol.id))) {
|
|
147
191
|
continue;
|
|
148
192
|
}
|
|
149
193
|
const endLine = Math.max(symbol.position.line, symbol.endLine);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The generated-file class on TypeScript: every declaration in a file below a directory
|
|
3
|
+
* an applied convention row names as generated is retained, because the framework owns
|
|
4
|
+
* the file and a deletion there is undone at the next generation.
|
|
5
|
+
*/
|
|
6
|
+
/** The generated-file detector, recording each file's first line. */
|
|
7
|
+
export function generatedFile(input) {
|
|
8
|
+
const generated = input.generated;
|
|
9
|
+
if (generated === undefined || generated.size === 0) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
return input.held.symbols.flatMap((symbol) => {
|
|
13
|
+
const row = generated.get(symbol.position.path);
|
|
14
|
+
return symbol.kind === "file" || row === undefined
|
|
15
|
+
? []
|
|
16
|
+
: [
|
|
17
|
+
{
|
|
18
|
+
id: symbol.id,
|
|
19
|
+
detail: `generated below a directory the convention row ${row} names`,
|
|
20
|
+
site: { path: symbol.position.path, line: 1, column: 1 },
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
});
|
|
24
|
+
}
|
package/dist/src/graph.js
CHANGED
|
@@ -32,20 +32,26 @@ 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 the configuration it loads
|
|
36
|
-
*
|
|
37
|
-
*
|
|
35
|
+
* tool reads the configuration it loads and the files its strings name, a runtime runs a
|
|
36
|
+
* test file, a setup file, a worker, a command and a script, the manifest names what
|
|
37
|
+
* enters the package, an alias global stands for the export it names, and an entry file,
|
|
38
|
+
* a configured root and a pattern assert a caller. A library's published API only supposes
|
|
38
39
|
* a consumer the run may not see, so it is live under reachability alone.
|
|
39
40
|
*/
|
|
40
41
|
const NAMES_A_CALLER = {
|
|
41
42
|
"entry-file": true,
|
|
43
|
+
convention: true,
|
|
42
44
|
"manifest-entry": true,
|
|
43
45
|
"manifest-binary": true,
|
|
46
|
+
script: true,
|
|
44
47
|
"published-api": false,
|
|
45
48
|
configured: true,
|
|
46
49
|
pattern: true,
|
|
50
|
+
"type-error": true,
|
|
51
|
+
"type-query-alias": true,
|
|
52
|
+
"configuration-file": true,
|
|
53
|
+
"configuration-string": true,
|
|
47
54
|
"test-runner": true,
|
|
48
|
-
"lint-configuration": true,
|
|
49
55
|
"mutation-testing": true,
|
|
50
56
|
"browser-tests": true,
|
|
51
57
|
worker: true,
|
|
@@ -65,7 +71,8 @@ export function namesACaller(kind) {
|
|
|
65
71
|
* from such a declaration still counts toward its target, because reference counting
|
|
66
72
|
* asks whether any declaration names the target. A root naming nothing is dropped.
|
|
67
73
|
*/
|
|
68
|
-
export function graphOf(symbols, references, roots, testFiles) {
|
|
74
|
+
export function graphOf(symbols, references, roots, testFiles, supportFiles = []) {
|
|
75
|
+
const supports = new Set(supportFiles);
|
|
69
76
|
const index = new Map(symbols.map((symbol, at) => [symbol.id, at]));
|
|
70
77
|
const at = (id) => index.get(id) ?? OUTSIDE;
|
|
71
78
|
const tests = new Set(testFiles);
|
|
@@ -114,6 +121,7 @@ export function graphOf(symbols, references, roots, testFiles) {
|
|
|
114
121
|
made,
|
|
115
122
|
parent,
|
|
116
123
|
test: symbols.map((symbol) => tests.has(symbol.position.path)),
|
|
124
|
+
support: symbols.map((symbol) => supports.has(symbol.position.path)),
|
|
117
125
|
subject: symbols.map((symbol) => swept(symbol.kind)),
|
|
118
126
|
exportsOf,
|
|
119
127
|
rooted,
|
|
@@ -12,19 +12,13 @@
|
|
|
12
12
|
* the contextual lookup has none, so it is asked only of a value whose type is a class
|
|
13
13
|
* of the target, and each distinct pair, interface and class is asked once.
|
|
14
14
|
*/
|
|
15
|
-
import {
|
|
15
|
+
import { isClassDeclaration, isClassExpression, isClassStaticBlockDeclaration, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isHeritageClause, isMethodDeclaration, isObjectLiteralExpression, isSetAccessorDeclaration, ModifierFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
16
16
|
import { SymbolFlags } from "@typescript/native/unstable/sync";
|
|
17
17
|
import { aliasChains } from "./alias-chain.js";
|
|
18
18
|
import { nodeKey } from "./inventory.js";
|
|
19
19
|
import { renderPosition } from "./position.js";
|
|
20
20
|
import { must } from "./query.js";
|
|
21
|
-
|
|
22
|
-
const STORES = new Set([
|
|
23
|
-
SyntaxKind.EqualsToken,
|
|
24
|
-
SyntaxKind.QuestionQuestionEqualsToken,
|
|
25
|
-
SyntaxKind.BarBarEqualsToken,
|
|
26
|
-
SyntaxKind.AmpersandAmpersandEqualsToken,
|
|
27
|
-
]);
|
|
21
|
+
import { handedOn, valuesOf } from "./value-flow.js";
|
|
28
22
|
/** Whether one node declares a static member, whose `this` is the class itself. */
|
|
29
23
|
function isStatic(node) {
|
|
30
24
|
const flags = node.modifierFlags ?? 0;
|
|
@@ -37,66 +31,6 @@ function isObjectLiteralMethod(node) {
|
|
|
37
31
|
isSetAccessorDeclaration(node)) &&
|
|
38
32
|
isObjectLiteralExpression(node.parent));
|
|
39
33
|
}
|
|
40
|
-
/** Whether a declaration writes its type, so its initializer is read in that type's context. */
|
|
41
|
-
function writesType(node) {
|
|
42
|
-
return node.type !== undefined;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* The values one written expression hands on: the expression itself, or what it
|
|
46
|
-
* evaluates to through parentheses, a conditional and the operators whose result is
|
|
47
|
-
* one of their operands.
|
|
48
|
-
*/
|
|
49
|
-
function valuesOf(expression) {
|
|
50
|
-
if (isParenthesizedExpression(expression)) {
|
|
51
|
-
return valuesOf(expression.expression);
|
|
52
|
-
}
|
|
53
|
-
if (isConditionalExpression(expression)) {
|
|
54
|
-
return [...valuesOf(expression.whenTrue), ...valuesOf(expression.whenFalse)];
|
|
55
|
-
}
|
|
56
|
-
if (isBinaryExpression(expression)) {
|
|
57
|
-
const operator = expression.operatorToken.kind;
|
|
58
|
-
if (operator === SyntaxKind.QuestionQuestionToken || operator === SyntaxKind.BarBarToken) {
|
|
59
|
-
return [...valuesOf(expression.left), ...valuesOf(expression.right)];
|
|
60
|
-
}
|
|
61
|
-
if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.CommaToken) {
|
|
62
|
-
return valuesOf(expression.right);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return [expression];
|
|
66
|
-
}
|
|
67
|
-
/** The expressions one node hands to a position typed by its context. */
|
|
68
|
-
function handedOn(node) {
|
|
69
|
-
if ((isVariableDeclaration(node) || isPropertyDeclaration(node) || isParameterDeclaration(node)) &&
|
|
70
|
-
node.initializer !== undefined &&
|
|
71
|
-
writesType(node)) {
|
|
72
|
-
return [node.initializer];
|
|
73
|
-
}
|
|
74
|
-
if (isBinaryExpression(node) && STORES.has(node.operatorToken.kind)) {
|
|
75
|
-
return [node.right];
|
|
76
|
-
}
|
|
77
|
-
if (isCallExpression(node) || isNewExpression(node)) {
|
|
78
|
-
return (node.arguments ?? []).filter((argument) => !isSpreadElement(argument));
|
|
79
|
-
}
|
|
80
|
-
if (isReturnStatement(node)) {
|
|
81
|
-
return node.expression === undefined ? [] : [node.expression];
|
|
82
|
-
}
|
|
83
|
-
if (isArrowFunction(node)) {
|
|
84
|
-
return node.body.kind === SyntaxKind.Block ? [] : [node.body];
|
|
85
|
-
}
|
|
86
|
-
if (isArrayLiteralExpression(node)) {
|
|
87
|
-
return node.elements.filter((element) => !isSpreadElement(element) && element.kind !== SyntaxKind.OmittedExpression);
|
|
88
|
-
}
|
|
89
|
-
if (isPropertyAssignment(node)) {
|
|
90
|
-
return [node.initializer];
|
|
91
|
-
}
|
|
92
|
-
if (isShorthandPropertyAssignment(node)) {
|
|
93
|
-
return isIdentifier(node.name) ? [node.name] : [];
|
|
94
|
-
}
|
|
95
|
-
if (isAsExpression(node) || isSatisfiesExpression(node)) {
|
|
96
|
-
return [node.expression];
|
|
97
|
-
}
|
|
98
|
-
return [];
|
|
99
|
-
}
|
|
100
34
|
/**
|
|
101
35
|
* The classes, the `implements` entries and the flowing values of one project's own
|
|
102
36
|
* files. `this` is the class an instance member belongs to, so a function that is not
|
package/dist/src/inventory.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isClassDeclaration, isComputedPropertyName, isEnumDeclaration, isEnumMember, isExportAssignment, isExportDeclaration, isFunctionDeclaration, isGetAccessorDeclaration, isIdentifier, isInterfaceDeclaration, isIntersectionTypeNode, isMethodDeclaration, isMethodSignatureDeclaration, isModuleBlock, isModuleDeclaration, isNamespaceExport, isParenthesizedTypeNode, isPrivateIdentifier, isPropertyDeclaration, isPropertySignatureDeclaration, isSetAccessorDeclaration, isTypeAliasDeclaration, isTypeLiteralNode, isUnionTypeNode, isVariableStatement, ModifierFlags, } from "@typescript/native/unstable/ast";
|
|
2
2
|
import { dirnamePath, joinPath, relativePath } from "./paths.js";
|
|
3
|
-
import { byPosition, positionKey, renderPosition } from "./position.js";
|
|
3
|
+
import { byPosition, positionKey, renderPosition, writtenLength, } from "./position.js";
|
|
4
4
|
import { computedComponent, nameComponent, renderRef } from "./ref.js";
|
|
5
5
|
import { isAnswered, UNANSWERED } from "./query.js";
|
|
6
6
|
const MANIFEST = "package.json";
|
|
@@ -69,7 +69,10 @@ function modifiersOf(node) {
|
|
|
69
69
|
function typeParametersOf(node) {
|
|
70
70
|
if (isFunctionDeclaration(node) ||
|
|
71
71
|
isMethodDeclaration(node) ||
|
|
72
|
-
isMethodSignatureDeclaration(node)
|
|
72
|
+
isMethodSignatureDeclaration(node) ||
|
|
73
|
+
isClassDeclaration(node) ||
|
|
74
|
+
isInterfaceDeclaration(node) ||
|
|
75
|
+
isTypeAliasDeclaration(node)) {
|
|
73
76
|
return node.typeParameters ?? [];
|
|
74
77
|
}
|
|
75
78
|
return [];
|
|
@@ -273,7 +276,9 @@ export function inventory(project, host, targetRoot) {
|
|
|
273
276
|
typeParameter: undefined,
|
|
274
277
|
});
|
|
275
278
|
exportScopes.push({ owner: fileRecord, nameNode: file });
|
|
276
|
-
|
|
279
|
+
// A statement a component file's module appends is not written in the file.
|
|
280
|
+
const written = writtenLength(file);
|
|
281
|
+
walkStatements(file.statements.filter((statement) => statement.end <= written), fileRecord);
|
|
277
282
|
}
|
|
278
283
|
/** One declaration of a module, a namespace or a container body. */
|
|
279
284
|
function walkStatements(statements, owner) {
|