@cplieger/deadset-ts 5.0.0 → 5.1.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 +145 -39
- package/dist/src/configuration-files.js +108 -4
- package/dist/src/conformance-record.js +3 -9
- package/dist/src/contextual-uses.js +247 -0
- package/dist/src/convention-rows.js +6 -0
- package/dist/src/conventions.js +123 -14
- package/dist/src/dependencies.js +8 -2
- package/dist/src/destructuring.js +6 -6
- package/dist/src/entry-points.js +8 -5
- package/dist/src/exempt-classes.js +6 -1
- package/dist/src/exempt.js +35 -16
- package/dist/src/file-facts.js +23 -20
- package/dist/src/findings/completion.js +27 -3
- 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 +40 -1
- package/dist/src/generated-file.js +24 -0
- package/dist/src/graph.js +1 -1
- package/dist/src/interface-satisfaction.js +2 -68
- package/dist/src/inventory.js +17 -4
- package/dist/src/kinds.js +4 -3
- package/dist/src/manifest.js +29 -4
- package/dist/src/outside-tests.js +139 -0
- package/dist/src/overrides.js +104 -0
- package/dist/src/paths.js +17 -0
- package/dist/src/query.js +15 -0
- package/dist/src/references.js +229 -11
- package/dist/src/resolve.js +12 -11
- package/dist/src/roots.js +121 -37
- package/dist/src/session.js +4 -3
- package/dist/src/stores.js +3 -0
- package/dist/src/template-field.js +60 -3
- package/dist/src/value-flow.js +74 -0
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +194 -49
- package/src/configuration-files.ts +137 -2
- package/src/conformance-record.ts +3 -10
- package/src/contextual-uses.ts +358 -0
- package/src/convention-rows.ts +8 -0
- package/src/conventions.ts +161 -13
- package/src/dependencies.ts +14 -1
- package/src/destructuring.ts +16 -6
- package/src/entry-points.ts +16 -3
- package/src/exempt-classes.ts +6 -1
- package/src/exempt.ts +67 -16
- package/src/file-facts.ts +26 -20
- package/src/findings/completion.ts +31 -3
- package/src/findings/interfaces.ts +9 -2
- package/src/findings/reads-and-writes.ts +70 -9
- package/src/findings/unused-declarations.ts +51 -1
- package/src/generated-file.ts +27 -0
- package/src/graph.ts +4 -4
- package/src/interface-satisfaction.ts +1 -92
- package/src/inventory.ts +19 -4
- package/src/kinds.ts +5 -3
- package/src/manifest.ts +40 -4
- package/src/outside-tests.ts +191 -0
- package/src/overrides.ts +136 -0
- package/src/paths.ts +18 -0
- package/src/query.ts +26 -0
- package/src/references.ts +294 -22
- package/src/resolve.ts +14 -13
- package/src/roots.ts +152 -44
- package/src/session.ts +4 -2
- package/src/stores.ts +9 -0
- package/src/template-field.ts +65 -3
- package/src/value-flow.ts +104 -0
- package/src/version.ts +1 -1
package/dist/src/exempt.js
CHANGED
|
@@ -88,11 +88,22 @@ function detected(exemptionClass, detect, input) {
|
|
|
88
88
|
* test file makes is none there.
|
|
89
89
|
*/
|
|
90
90
|
export function computeExemptions(input, detectors, holding) {
|
|
91
|
+
return detectExemptions(input, detectors, holding).records;
|
|
92
|
+
}
|
|
93
|
+
/** The records of {@link computeExemptions}, beside the test-file evidence it set aside. */
|
|
94
|
+
export function detectExemptions(input, detectors, holding) {
|
|
95
|
+
return heldRecords(evidenceRecords(input, detectors, holding.disabled), holding);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Every record one project's detectors found, one per piece of evidence, before the
|
|
99
|
+
* mode decides which of them hold: what {@link heldRecords} reads.
|
|
100
|
+
*/
|
|
101
|
+
export function evidenceRecords(input, detectors, disabled) {
|
|
91
102
|
const visibility = new Map(input.held.symbols.map((symbol) => [symbol.id, symbol.visibility]));
|
|
92
103
|
const found = [];
|
|
93
104
|
for (const exemptionClass of TS_EXEMPTION_CLASSES) {
|
|
94
105
|
const detect = detectors.get(exemptionClass);
|
|
95
|
-
if (detect === undefined ||
|
|
106
|
+
if (detect === undefined || disabled.has(exemptionClass)) {
|
|
96
107
|
continue;
|
|
97
108
|
}
|
|
98
109
|
const allowed = typescriptVisibilityOf(exemptionClass);
|
|
@@ -100,9 +111,6 @@ export function computeExemptions(input, detectors, holding) {
|
|
|
100
111
|
if (!retainable(allowed, visibility.get(evidence.id))) {
|
|
101
112
|
continue;
|
|
102
113
|
}
|
|
103
|
-
if (holding.mode.production && holding.testFiles.has(evidence.site.path)) {
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
114
|
found.push({
|
|
107
115
|
id: evidence.id,
|
|
108
116
|
class: exemptionClass,
|
|
@@ -112,7 +120,21 @@ export function computeExemptions(input, detectors, holding) {
|
|
|
112
120
|
});
|
|
113
121
|
}
|
|
114
122
|
}
|
|
115
|
-
return
|
|
123
|
+
return found;
|
|
124
|
+
}
|
|
125
|
+
/** The records that hold under one mode, beside the evidence a production mode found in a test file. */
|
|
126
|
+
export function heldRecords(records, holding) {
|
|
127
|
+
const found = [];
|
|
128
|
+
const inTestFiles = [];
|
|
129
|
+
for (const record of records) {
|
|
130
|
+
if (holding.mode.production && holding.testFiles.has(record.site.path)) {
|
|
131
|
+
inTestFiles.push(record);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
found.push(record);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { records: exemptionsOf(found), inTestFiles };
|
|
116
138
|
}
|
|
117
139
|
/** Two strings ordered bytewise. */
|
|
118
140
|
function compare(a, b) {
|
|
@@ -160,24 +182,21 @@ function byDeclaration(records) {
|
|
|
160
182
|
return grouped;
|
|
161
183
|
}
|
|
162
184
|
/**
|
|
163
|
-
* The records that held back a declaration some configuration
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
* on a declaration that a relation or a mark holds live in every configuration held
|
|
170
|
-
* nothing back. The order is the declarations' site order, and for one declaration
|
|
171
|
-
* the input's.
|
|
185
|
+
* The records that held back a declaration: one some configuration's graph, swept
|
|
186
|
+
* again with no exemption and its marks, judges a candidate, whatever another
|
|
187
|
+
* configuration answers, or one in `writeOnly`, which the run would report as written
|
|
188
|
+
* and never read without the record's read. A declaration a relation or a mark holds
|
|
189
|
+
* live in every configuration is held back by no record. The order is the
|
|
190
|
+
* declarations' site order, and for one declaration the input's.
|
|
172
191
|
*/
|
|
173
|
-
export function retainedIn(matrix, input) {
|
|
192
|
+
export function retainedIn(matrix, input, writeOnly = new Set()) {
|
|
174
193
|
const exempt = input.exempt ?? [];
|
|
175
194
|
if (exempt.length === 0) {
|
|
176
195
|
return [];
|
|
177
196
|
}
|
|
178
197
|
const bare = { marked: input.marked, mode: input.mode };
|
|
179
198
|
const dead = new Set(matrix.graphs.flatMap((graph) => sweep(graph, bare).candidates.map((candidate) => candidate.id)));
|
|
180
|
-
const held = byDeclaration(exempt.filter((record) => dead.has(record.id)));
|
|
199
|
+
const held = byDeclaration(exempt.filter((record) => dead.has(record.id) || writeOnly.has(record.id)));
|
|
181
200
|
return matrix.union.symbols.flatMap((symbol) => held.get(symbol.id) ?? []);
|
|
182
201
|
}
|
|
183
202
|
/**
|
package/dist/src/file-facts.js
CHANGED
|
@@ -26,12 +26,10 @@ function lastLine(text) {
|
|
|
26
26
|
return line;
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
30
|
-
* read is skipped, as discovery skips it
|
|
31
|
-
* with it: a tree the analysis may not enter holds no file it can report.
|
|
29
|
+
* The path below the target root of every source file there, in path order. A directory
|
|
30
|
+
* that cannot be read is skipped, as discovery skips it.
|
|
32
31
|
*/
|
|
33
|
-
export function
|
|
34
|
-
const scopeOf = packageScope(host, targetRoot);
|
|
32
|
+
export function sourcePaths(host, targetRoot) {
|
|
35
33
|
const found = [];
|
|
36
34
|
const pending = [""];
|
|
37
35
|
for (let dir = pending.pop(); dir !== undefined; dir = pending.pop()) {
|
|
@@ -50,24 +48,29 @@ export function sourceTree(host, targetRoot) {
|
|
|
50
48
|
}
|
|
51
49
|
continue;
|
|
52
50
|
}
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
let text;
|
|
57
|
-
try {
|
|
58
|
-
text = host.readFile(joinPath(targetRoot, path));
|
|
51
|
+
if (SOURCE_FILE.test(entry.name) && !DECLARATION_FILE.test(entry.name)) {
|
|
52
|
+
found.push(path);
|
|
59
53
|
}
|
|
60
|
-
catch {
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
found.push({
|
|
64
|
-
path,
|
|
65
|
-
ref: renderRef(scopeOf(path), { of: "module" }),
|
|
66
|
-
endLine: lastLine(text),
|
|
67
|
-
});
|
|
68
54
|
}
|
|
69
55
|
}
|
|
70
|
-
return found.sort((a, b) => (a
|
|
56
|
+
return found.sort((a, b) => (a < b ? -1 : 1));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Every source file below the target root, in path order. A file that cannot be read is
|
|
60
|
+
* skipped: a tree the analysis may not enter holds no file it can report.
|
|
61
|
+
*/
|
|
62
|
+
export function sourceTree(host, targetRoot) {
|
|
63
|
+
const scopeOf = packageScope(host, targetRoot);
|
|
64
|
+
return sourcePaths(host, targetRoot).flatMap((path) => {
|
|
65
|
+
let text;
|
|
66
|
+
try {
|
|
67
|
+
text = host.readFile(joinPath(targetRoot, path));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
return [{ path, ref: renderRef(scopeOf(path), { of: "module" }), endLine: lastLine(text) }];
|
|
73
|
+
});
|
|
71
74
|
}
|
|
72
75
|
/** Whether one file changes what the program sees by being included in it. */
|
|
73
76
|
function actsByInclusion(file) {
|
|
@@ -116,7 +116,8 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
116
116
|
const classOf = reachabilityClasses(input);
|
|
117
117
|
const componentOf = findingComponents(swept, input.config.reporters.cascade);
|
|
118
118
|
let minted = swept.sweep.components.length;
|
|
119
|
-
|
|
119
|
+
const dead = new Set(swept.sweep.components.map((component) => component.id));
|
|
120
|
+
const done = findings.map((finding) => {
|
|
120
121
|
const row = kinds.get(finding.code);
|
|
121
122
|
if (row === undefined) {
|
|
122
123
|
throw new Error(`${finding.code} names no live row of the issue-kind vocabulary`);
|
|
@@ -151,6 +152,7 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
151
152
|
? union.made[at]
|
|
152
153
|
: { production: candidate.productionRefs, test: candidate.testRefs };
|
|
153
154
|
const reachabilityClass = judged === undefined ? "certain" : classOf(judged.id);
|
|
155
|
+
const generated = input.files.generated?.has(finding.position.path) === true;
|
|
154
156
|
return {
|
|
155
157
|
code: finding.code,
|
|
156
158
|
kind: row.name,
|
|
@@ -161,7 +163,7 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
161
163
|
confidence: capped(reachabilityClass, row.maxClass),
|
|
162
164
|
...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
|
|
163
165
|
testOnly: judged !== undefined && counts?.production === 0 && counts.test > 0,
|
|
164
|
-
generated
|
|
166
|
+
generated,
|
|
165
167
|
component,
|
|
166
168
|
retainedBy: [],
|
|
167
169
|
configurations: candidate?.configurations ??
|
|
@@ -169,12 +171,34 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
169
171
|
? swept.matrix.configurations
|
|
170
172
|
: (swept.matrix.heldIn[at] ?? swept.matrix.configurations)),
|
|
171
173
|
consumersLoaded: input.boundary.consumers.loaded,
|
|
172
|
-
fixability: row.fixability,
|
|
174
|
+
fixability: generated ? "none" : row.fixability,
|
|
173
175
|
severity: severityOf(input, finding.code, row.defaultSeverity),
|
|
174
176
|
message: finding.message,
|
|
175
177
|
details: finding.details ?? {},
|
|
176
178
|
};
|
|
177
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
|
+
});
|
|
178
202
|
}
|
|
179
203
|
/** Whether the dials withhold a finding by its own severity or confidence. */
|
|
180
204
|
function dialed(finding, minConfidence) {
|
|
@@ -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
|
}
|
|
@@ -114,6 +114,43 @@ function messageOf(code, judged) {
|
|
|
114
114
|
}
|
|
115
115
|
return `${subject} has no reference in the target`;
|
|
116
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"]);
|
|
117
154
|
/** The findings of the unused-declarations family, `DS1000` to `DS1099`. */
|
|
118
155
|
export const unusedDeclarations = (input) => familyFindings(input, false);
|
|
119
156
|
/**
|
|
@@ -127,6 +164,7 @@ function familyFindings(input, fallen) {
|
|
|
127
164
|
const union = input.swept.matrix.union;
|
|
128
165
|
const dead = new Set(input.swept.sweep.candidates.map((candidate) => candidate.id));
|
|
129
166
|
const unreachable = unreachableExports(input);
|
|
167
|
+
let brands;
|
|
130
168
|
const found = [];
|
|
131
169
|
for (const candidate of input.swept.sweep.candidates) {
|
|
132
170
|
const at = union.at(candidate.id);
|
|
@@ -148,7 +186,8 @@ function familyFindings(input, fallen) {
|
|
|
148
186
|
unreachable: unreachable.has(symbol.id),
|
|
149
187
|
};
|
|
150
188
|
const code = codeOf(judged);
|
|
151
|
-
if (code === undefined
|
|
189
|
+
if (code === undefined ||
|
|
190
|
+
(code === "DS1003" && (brands ??= brandMembers(input)).has(symbol.id))) {
|
|
152
191
|
continue;
|
|
153
192
|
}
|
|
154
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
|
@@ -94,7 +94,7 @@ export function graphOf(symbols, references, roots, testFiles, supportFiles = []
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
const edge = { to, test: reference.test, evaluation: reference.use === "evaluation" };
|
|
97
|
-
if (reference.consumer !== undefined) {
|
|
97
|
+
if (reference.consumer !== undefined || reference.unheld === true) {
|
|
98
98
|
consumed.push(edge);
|
|
99
99
|
continue;
|
|
100
100
|
}
|
|
@@ -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
|
@@ -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 [];
|
|
@@ -210,7 +213,18 @@ function nameNodeOf(node) {
|
|
|
210
213
|
* needs the declaration this enumeration made for it.
|
|
211
214
|
*/
|
|
212
215
|
export function nodeKey(file, node) {
|
|
213
|
-
return `${file.fileName}:${String(node.pos)}:${String(node.end)}`;
|
|
216
|
+
return `${file.fileName}:${String(node.pos)}:${String(node.end)}:${String(node.kind)}`;
|
|
217
|
+
}
|
|
218
|
+
/** `export default`, up to the keyword that stands where an unnamed default export's name would. */
|
|
219
|
+
const DEFAULT_EXPORT = /^export\s+(?=default\b)/u;
|
|
220
|
+
/**
|
|
221
|
+
* Where an unnamed declaration is placed: at `default` when it is the default export, so it
|
|
222
|
+
* never shares the file's own position, else at its first token.
|
|
223
|
+
*/
|
|
224
|
+
function unnamedStart(node) {
|
|
225
|
+
const start = node.getStart();
|
|
226
|
+
const prefix = DEFAULT_EXPORT.exec(node.getText());
|
|
227
|
+
return prefix === null ? start : start + prefix[0].length;
|
|
214
228
|
}
|
|
215
229
|
/** The kind a class element takes: a method, or a property or accessor. */
|
|
216
230
|
function classMemberKind(node) {
|
|
@@ -284,12 +298,11 @@ export function inventory(project, host, targetRoot) {
|
|
|
284
298
|
}
|
|
285
299
|
}
|
|
286
300
|
function declare(node, owner, kind, name, named) {
|
|
287
|
-
const at = name ?? node;
|
|
288
301
|
const record = keep({
|
|
289
302
|
file: owner.file,
|
|
290
303
|
node,
|
|
291
304
|
module: owner.module,
|
|
292
|
-
position: renderPosition(owner.file, targetRoot,
|
|
305
|
+
position: renderPosition(owner.file, targetRoot, name?.getStart() ?? unnamedStart(node)),
|
|
293
306
|
endLine: renderPosition(owner.file, targetRoot, Math.max(node.end - 1, 0)).line,
|
|
294
307
|
kind,
|
|
295
308
|
parentChain: owner.kind === "file" ? [] : chainOf(owner),
|
package/dist/src/kinds.js
CHANGED
|
@@ -30,6 +30,7 @@ const ROWS = [
|
|
|
30
30
|
name: "unused-member",
|
|
31
31
|
languages: ["go", "ts"],
|
|
32
32
|
rule: "A struct field, class member or type member with no reference, a private member included. A member referenced only from its own declaration site counts as unreferenced.",
|
|
33
|
+
precondition: "A TypeScript member that no value writes and nothing reads, whose type names a type parameter of its declaring interface, class or type alias, is not reported when every part of that declaration that names the type parameter is a member no value writes and nothing reads. TypeScript compares object types by their members, so such members are what make two instantiations of their type distinct types, and deleting them leaves the type parameter phantom and lets one instantiation stand for another. A member a value writes is decided by the rule of DS1301 instead, whatever its type names.",
|
|
33
34
|
defaultSeverity: "deny",
|
|
34
35
|
maxClass: "certain",
|
|
35
36
|
fixability: "deletable",
|
|
@@ -70,7 +71,7 @@ const ROWS = [
|
|
|
70
71
|
name: "unnecessary-export",
|
|
71
72
|
languages: ["go", "ts"],
|
|
72
73
|
rule: "An exported symbol whose every reference is inside the symbol's own package or module, reported as a candidate for unexporting. The subject is a package-level declaration or a method, and three subjects are excluded: an interface method, whose exportedness is the contract of the interface that declares it; a struct field, which an encoder reads by name; and a method that satisfies an interface some symbol uses as a type, which cannot be unexported without its type ceasing to satisfy that interface. The finding names the narrower visibility the references support. A declared cross-language edge counts as an out-of-package reference; where the edge's other side is unknown to the analyzer the finding is emitted pending.",
|
|
73
|
-
precondition: "Closed world only. Reported always for a main package and an internal/ directory tree, and for a published package only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first.",
|
|
74
|
+
precondition: "Closed world only. Reported always for a main package and an internal/ directory tree, and for a published package only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first. A type that a parameter or a result of an exported function or method of its own package names is not reported when a reference from outside the package references that function or method, because the caller holds values of the type, and unexporting it would leave an exported signature naming a type its callers cannot name.",
|
|
74
75
|
defaultSeverity: "warn",
|
|
75
76
|
maxClass: "certain",
|
|
76
77
|
fixability: "narrowable",
|
|
@@ -81,7 +82,7 @@ const ROWS = [
|
|
|
81
82
|
name: "unnecessary-exposure",
|
|
82
83
|
languages: ["go"],
|
|
83
84
|
rule: "An exported symbol of a non-internal package whose every reference is inside the target module, reported as a candidate for relocation behind an internal boundary. The subject is a package-level declaration or a method, and three subjects are excluded: an interface method, whose exportedness is the contract of the interface that declares it; a struct field, which an encoder reads by name; and a method that satisfies an interface some symbol uses as a type, which cannot be unexported without its type ceasing to satisfy that interface. The finding names the narrower visibility the references support. A declared cross-language edge counts as an out-of-package reference; where the edge's other side is unknown to the analyzer the finding is emitted pending.",
|
|
84
|
-
precondition: "Closed world only. Reported always for a main package and an internal/ directory tree, and for a published package only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first.",
|
|
85
|
+
precondition: "Closed world only. Reported always for a main package and an internal/ directory tree, and for a published package only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first. A type that a parameter or a result of an exported function or method of its own package names is not reported when a reference from outside the target module references that function or method, because the caller holds values of the type, and moving it behind an internal boundary would leave an exported signature naming a type its callers cannot name.",
|
|
85
86
|
defaultSeverity: "warn",
|
|
86
87
|
maxClass: "certain",
|
|
87
88
|
fixability: "narrowable",
|
|
@@ -143,7 +144,7 @@ const ROWS = [
|
|
|
143
144
|
code: "DS1301",
|
|
144
145
|
name: "write-only-symbol",
|
|
145
146
|
languages: ["go", "ts"],
|
|
146
|
-
rule: "A package-level or module-level variable, struct field, class member or collection that production code writes and never reads. In production mode a read from a test file counts as no read. A comparison of struct values reads every field of the type: in Go, a value of a struct type used as an operand of `==` or `!=`, as a map key, or as a `switch` tag or `case` expression is a read of every field of that type, transitively through the fields of every struct type it holds, recorded at the comparison, because equality reads every field and a field that decides equality carries information. In TypeScript a comparison of two object references reads no member, because `===` compares identity and never a member. The finding names each write position, so the deletion set is visible.",
|
|
147
|
+
rule: "A package-level or module-level variable, struct field, class member, type member or collection that production code writes and never reads. In production mode a read from a test file counts as no read. A comparison of struct values reads every field of the type: in Go, a value of a struct type used as an operand of `==` or `!=`, as a map key, or as a `switch` tag or `case` expression is a read of every field of that type, transitively through the fields of every struct type it holds, recorded at the comparison, because equality reads every field and a field that decides equality carries information. In TypeScript a comparison of two object references reads no member, because `===` compares identity and never a member. In TypeScript a property of an object literal writes the member of the same name that the literal's contextual type declares, whether or not the compiler checks the literal for excess properties. Where that contextual type is a union or an intersection, the property writes the member of that name on each constituent that declares one. So a literal that a declaration annotates, a function returns, a call passes or a `satisfies` expression checks writes the members of the type it is given. A store through the value a field or a member holds reads that field or member rather than writing it, because the store lands in storage every other holder of the value reads. In Go that store is an assignment, an increment, a compound assignment or a `delete` whose target is reached through a struct field holding a map, a slice or a pointer. Examples are `s.f[k] = v`, `s.f.x = v` and `delete(s.f, k)`. The same store through a field holding an array or a struct is a store into the field itself. TypeScript holds every object by reference, so an element or property store through a member's value reads the member. The finding names each write position, so the deletion set is visible. Deleting the subject and its writes also deletes every declaration whose only uses they hold. Such a declaration is a type parameter of the subject's declaring type that the subject's type alone names, or an import binding the writes alone use. The finding's message names each such declaration.",
|
|
147
148
|
defaultSeverity: "deny",
|
|
148
149
|
maxClass: "certain",
|
|
149
150
|
fixability: "deletable",
|