@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
|
@@ -62,23 +62,76 @@ function findingAbout(
|
|
|
62
62
|
interface Uses {
|
|
63
63
|
readonly writes: FindingPosition[];
|
|
64
64
|
reads: number;
|
|
65
|
+
/** The import bindings only the writes use. */
|
|
66
|
+
readonly alone: Set<string>;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
/**
|
|
68
70
|
* Whether one declaration is a subject of the write-only kind: a module-level variable,
|
|
69
|
-
* or a class member that is state rather than code. A getter
|
|
70
|
-
* store into one runs it.
|
|
71
|
+
* a member of an object type, or a class member that is state rather than code. A getter
|
|
72
|
+
* or a setter is code, so a store into one runs it.
|
|
71
73
|
*/
|
|
72
74
|
function writable(run: Run, symbol: InventorySymbol): boolean {
|
|
73
|
-
if (symbol.kind === "variable") {
|
|
75
|
+
if (symbol.kind === "variable" || symbol.kind === "type-member") {
|
|
74
76
|
return true;
|
|
75
77
|
}
|
|
76
78
|
return symbol.kind === "class-member" && !run.input.stores.accessors.has(symbol.id);
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
/** The reader's word for each kind of the write-only kind's subject. */
|
|
82
|
+
const SUBJECT_WORDS: Readonly<Partial<Record<InventorySymbol["kind"], string>>> = {
|
|
83
|
+
variable: "variable",
|
|
84
|
+
"class-member": "member",
|
|
85
|
+
"type-member": "type member",
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The type parameters of one member's declaring type that only the member names, which
|
|
90
|
+
* deleting the member leaves unused.
|
|
91
|
+
*/
|
|
92
|
+
function parametersOnlyNamedBy(
|
|
93
|
+
run: Run,
|
|
94
|
+
namedBy: ReadonlyMap<string, ReadonlySet<string>>,
|
|
95
|
+
member: InventorySymbol,
|
|
96
|
+
): readonly string[] {
|
|
97
|
+
return [...run.symbols.values()]
|
|
98
|
+
.filter((symbol) => {
|
|
99
|
+
const from = namedBy.get(symbol.id);
|
|
100
|
+
return (
|
|
101
|
+
symbol.kind === "type-parameter" &&
|
|
102
|
+
symbol.parent === member.parent &&
|
|
103
|
+
from?.size === 1 &&
|
|
104
|
+
from.has(member.id)
|
|
105
|
+
);
|
|
106
|
+
})
|
|
107
|
+
.map((symbol) => symbol.name);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** What one write-only finding says, the declarations its deletion takes with it included. */
|
|
111
|
+
function writeOnlyMessage(
|
|
112
|
+
run: Run,
|
|
113
|
+
namedBy: ReadonlyMap<string, ReadonlySet<string>>,
|
|
114
|
+
symbol: InventorySymbol,
|
|
115
|
+
held: Uses,
|
|
116
|
+
): string {
|
|
117
|
+
const writes = held.writes.length === 1 ? "once" : `at ${String(held.writes.length)} positions`;
|
|
118
|
+
const read = run.input.stores.readInTests.has(symbol.id)
|
|
119
|
+
? "read only from test files"
|
|
120
|
+
: "never read";
|
|
121
|
+
const cascade = [
|
|
122
|
+
...parametersOnlyNamedBy(run, namedBy, symbol).map((name) => `type parameter ${name}`),
|
|
123
|
+
...[...held.alone].sort().map((name) => `import ${name}`),
|
|
124
|
+
];
|
|
125
|
+
const word = SUBJECT_WORDS[symbol.kind] ?? "member";
|
|
126
|
+
const message = `${word} ${symbol.name} is written ${writes} and ${read}`;
|
|
127
|
+
return cascade.length === 0
|
|
128
|
+
? message
|
|
129
|
+
: `${message}, and deleting it with its writes deletes ${cascade.join(" and ")} too`;
|
|
130
|
+
}
|
|
131
|
+
|
|
79
132
|
/**
|
|
80
|
-
* `DS1301`: a module-level variable or a class member the counted
|
|
81
|
-
* and never read, named with every position written.
|
|
133
|
+
* `DS1301`: a module-level variable, a type member or a class member the counted
|
|
134
|
+
* references store into and never read, named with every position written.
|
|
82
135
|
*
|
|
83
136
|
* The subject is a live declaration: a store is a reference, so the sweep holds a
|
|
84
137
|
* written one live and its candidates are never this kind's. Every reference that is
|
|
@@ -91,7 +144,7 @@ function writeOnlySymbols(run: Run): readonly Finding[] {
|
|
|
91
144
|
for (const reference of stores.references) {
|
|
92
145
|
let held = uses.get(reference.to);
|
|
93
146
|
if (held === undefined) {
|
|
94
|
-
held = { writes: [], reads: 0 };
|
|
147
|
+
held = { writes: [], reads: 0, alone: new Set() };
|
|
95
148
|
uses.set(reference.to, held);
|
|
96
149
|
}
|
|
97
150
|
// A consumer's store is written at a position no report of the target can name, so
|
|
@@ -100,6 +153,9 @@ function writeOnlySymbols(run: Run): readonly Finding[] {
|
|
|
100
153
|
held.reads += 1;
|
|
101
154
|
continue;
|
|
102
155
|
}
|
|
156
|
+
for (const name of reference.alone ?? []) {
|
|
157
|
+
held.alone.add(name);
|
|
158
|
+
}
|
|
103
159
|
const at = reference.position;
|
|
104
160
|
const key = positionKey(at);
|
|
105
161
|
if (!held.writes.some((written) => positionKey(written) === key)) {
|
|
@@ -107,6 +163,13 @@ function writeOnlySymbols(run: Run): readonly Finding[] {
|
|
|
107
163
|
}
|
|
108
164
|
}
|
|
109
165
|
|
|
166
|
+
const namedBy = new Map<string, Set<string>>();
|
|
167
|
+
for (const reference of stores.references) {
|
|
168
|
+
if (run.symbols.get(reference.to)?.kind === "type-parameter") {
|
|
169
|
+
namedBy.set(reference.to, (namedBy.get(reference.to) ?? new Set()).add(reference.from));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
110
173
|
const found: Finding[] = [];
|
|
111
174
|
for (const symbol of matrix.union.symbols) {
|
|
112
175
|
const held = uses.get(symbol.id);
|
|
@@ -120,10 +183,8 @@ function writeOnlySymbols(run: Run): readonly Finding[] {
|
|
|
120
183
|
) {
|
|
121
184
|
continue;
|
|
122
185
|
}
|
|
123
|
-
const writes = held.writes.length === 1 ? "once" : `at ${String(held.writes.length)} positions`;
|
|
124
|
-
const word = symbol.kind === "variable" ? "variable" : "member";
|
|
125
186
|
found.push(
|
|
126
|
-
findingAbout(symbol, "DS1301",
|
|
187
|
+
findingAbout(symbol, "DS1301", writeOnlyMessage(run, namedBy, symbol, held), {
|
|
127
188
|
writePositions: held.writes,
|
|
128
189
|
}),
|
|
129
190
|
);
|
|
@@ -52,6 +52,8 @@ interface Judged {
|
|
|
52
52
|
readonly symbol: InventorySymbol;
|
|
53
53
|
/** Whether a test file declares it. */
|
|
54
54
|
readonly test: boolean;
|
|
55
|
+
/** Whether a test-support file declares it, which test code keeps live. */
|
|
56
|
+
readonly support: boolean;
|
|
55
57
|
/** Whether its container is itself dead, so it falls with the container. */
|
|
56
58
|
readonly containerDead: boolean;
|
|
57
59
|
readonly deprecated: boolean;
|
|
@@ -62,8 +64,9 @@ interface Judged {
|
|
|
62
64
|
/**
|
|
63
65
|
* The code one candidate is reported under, or none, each symbol once under the most
|
|
64
66
|
* specific code: a test of dead code first; no other test-file declaration, member of a
|
|
65
|
-
* dead container or enum member nothing names
|
|
66
|
-
*
|
|
67
|
+
* dead container or enum member nothing names, nor a test-support declaration test code
|
|
68
|
+
* references; then deprecation, a test reference, and the unreferenced codes, an
|
|
69
|
+
* unimportable unused export being the unreachable kind's.
|
|
67
70
|
*/
|
|
68
71
|
function codeOf(judged: Judged): string | undefined {
|
|
69
72
|
const { candidate, symbol } = judged;
|
|
@@ -77,6 +80,9 @@ function codeOf(judged: Judged): string | undefined {
|
|
|
77
80
|
if (symbol.kind === "enum-member" && !referenced) {
|
|
78
81
|
return undefined;
|
|
79
82
|
}
|
|
83
|
+
if (judged.support && candidate.testRefs > 0) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
80
86
|
if (candidate.productionRefs === 0 && judged.deprecated) {
|
|
81
87
|
return "DS1006";
|
|
82
88
|
}
|
|
@@ -136,6 +142,52 @@ function messageOf(code: string, judged: Judged): string {
|
|
|
136
142
|
return `${subject} has no reference in the target`;
|
|
137
143
|
}
|
|
138
144
|
|
|
145
|
+
/**
|
|
146
|
+
* The members nothing writes or reads that are brands: each names a type parameter of
|
|
147
|
+
* its declaring type that no other part of the declaration names, other than another
|
|
148
|
+
* such member. They are what make two instantiations of the type distinct types, so
|
|
149
|
+
* deleting them would let one instantiation stand for another.
|
|
150
|
+
*/
|
|
151
|
+
function brandMembers(input: EmitterInput): ReadonlySet<string> {
|
|
152
|
+
const union = input.swept.matrix.union;
|
|
153
|
+
const unreferenced = new Map(
|
|
154
|
+
input.swept.sweep.candidates
|
|
155
|
+
.filter((candidate) => candidate.productionRefs + candidate.testRefs === 0)
|
|
156
|
+
.map((candidate) => [candidate.id, candidate]),
|
|
157
|
+
);
|
|
158
|
+
const namedBy = new Map<string, Set<string>>();
|
|
159
|
+
for (const reference of input.stores.references) {
|
|
160
|
+
const to = union.symbols[union.at(reference.to)];
|
|
161
|
+
if (to?.kind === "type-parameter") {
|
|
162
|
+
namedBy.set(to.id, (namedBy.get(to.id) ?? new Set()).add(reference.from));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const brands = new Set<string>();
|
|
166
|
+
for (const [parameter, from] of namedBy) {
|
|
167
|
+
const owner = union.symbols[union.at(parameter)]?.parent;
|
|
168
|
+
const members = [...from].map((id) => union.symbols[union.at(id)]);
|
|
169
|
+
if (
|
|
170
|
+
members.every(
|
|
171
|
+
(member) =>
|
|
172
|
+
member !== undefined &&
|
|
173
|
+
member.parent === owner &&
|
|
174
|
+
BRANDS.has(member.kind) &&
|
|
175
|
+
unreferenced.has(member.id),
|
|
176
|
+
)
|
|
177
|
+
) {
|
|
178
|
+
for (const member of members) {
|
|
179
|
+
if (member !== undefined) {
|
|
180
|
+
brands.add(member.id);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return brands;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The kinds of member a brand is: a data member of a type or of a class. */
|
|
189
|
+
const BRANDS: ReadonlySet<SymbolKind> = new Set(["type-member", "class-member"]);
|
|
190
|
+
|
|
139
191
|
/** The findings of the unused-declarations family, `DS1000` to `DS1099`. */
|
|
140
192
|
export const unusedDeclarations: Emitter = (input) => familyFindings(input, false);
|
|
141
193
|
|
|
@@ -151,6 +203,7 @@ function familyFindings(input: EmitterInput, fallen: boolean): Finding[] {
|
|
|
151
203
|
const union = input.swept.matrix.union;
|
|
152
204
|
const dead = new Set(input.swept.sweep.candidates.map((candidate) => candidate.id));
|
|
153
205
|
const unreachable = unreachableExports(input);
|
|
206
|
+
let brands: ReadonlySet<string> | undefined;
|
|
154
207
|
|
|
155
208
|
const found: Finding[] = [];
|
|
156
209
|
for (const candidate of input.swept.sweep.candidates) {
|
|
@@ -167,12 +220,16 @@ function familyFindings(input: EmitterInput, fallen: boolean): Finding[] {
|
|
|
167
220
|
candidate,
|
|
168
221
|
symbol,
|
|
169
222
|
test: union.test[at] === true,
|
|
223
|
+
support: union.support[at] === true,
|
|
170
224
|
containerDead: containerDead && !fallen,
|
|
171
225
|
deprecated: input.deprecated.has(symbol.id),
|
|
172
226
|
unreachable: unreachable.has(symbol.id),
|
|
173
227
|
};
|
|
174
228
|
const code = codeOf(judged);
|
|
175
|
-
if (
|
|
229
|
+
if (
|
|
230
|
+
code === undefined ||
|
|
231
|
+
(code === "DS1003" && (brands ??= brandMembers(input)).has(symbol.id))
|
|
232
|
+
) {
|
|
176
233
|
continue;
|
|
177
234
|
}
|
|
178
235
|
const endLine = Math.max(symbol.position.line, symbol.endLine);
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
|
|
7
|
+
import type { DetectorInput, Evidence } from "./exempt.ts";
|
|
8
|
+
|
|
9
|
+
/** The generated-file detector, recording each file's first line. */
|
|
10
|
+
export function generatedFile<Brand>(input: DetectorInput<Brand>): readonly Evidence[] {
|
|
11
|
+
const generated = input.generated;
|
|
12
|
+
if (generated === undefined || generated.size === 0) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
return input.held.symbols.flatMap((symbol): Evidence[] => {
|
|
16
|
+
const row = generated.get(symbol.position.path);
|
|
17
|
+
return symbol.kind === "file" || row === undefined
|
|
18
|
+
? []
|
|
19
|
+
: [
|
|
20
|
+
{
|
|
21
|
+
id: symbol.id,
|
|
22
|
+
detail: `generated below a directory the convention row ${row} names`,
|
|
23
|
+
site: { path: symbol.position.path, line: 1, column: 1 },
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
});
|
|
27
|
+
}
|
package/src/graph.ts
CHANGED
|
@@ -39,20 +39,26 @@ const SWEPT: Readonly<Record<SymbolKind, boolean>> = {
|
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* Whether a root of each kind names a caller, which holds it live under both relations: a
|
|
42
|
-
* tool reads the configuration it loads
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* tool reads the configuration it loads and the files its strings name, a runtime runs a
|
|
43
|
+
* test file, a setup file, a worker, a command and a script, the manifest names what
|
|
44
|
+
* enters the package, an alias global stands for the export it names, and an entry file,
|
|
45
|
+
* a configured root and a pattern assert a caller. A library's published API only supposes
|
|
45
46
|
* a consumer the run may not see, so it is live under reachability alone.
|
|
46
47
|
*/
|
|
47
48
|
const NAMES_A_CALLER: Readonly<Record<RootKind, boolean>> = {
|
|
48
49
|
"entry-file": true,
|
|
50
|
+
convention: true,
|
|
49
51
|
"manifest-entry": true,
|
|
50
52
|
"manifest-binary": true,
|
|
53
|
+
script: true,
|
|
51
54
|
"published-api": false,
|
|
52
55
|
configured: true,
|
|
53
56
|
pattern: true,
|
|
57
|
+
"type-error": true,
|
|
58
|
+
"type-query-alias": true,
|
|
59
|
+
"configuration-file": true,
|
|
60
|
+
"configuration-string": true,
|
|
54
61
|
"test-runner": true,
|
|
55
|
-
"lint-configuration": true,
|
|
56
62
|
"mutation-testing": true,
|
|
57
63
|
"browser-tests": true,
|
|
58
64
|
worker: true,
|
|
@@ -103,6 +109,8 @@ export interface Graph {
|
|
|
103
109
|
readonly parent: readonly number[];
|
|
104
110
|
/** Per declaration, whether a test file holds it. */
|
|
105
111
|
readonly test: readonly boolean[];
|
|
112
|
+
/** Per declaration, whether a test-support file holds it. */
|
|
113
|
+
readonly support: readonly boolean[];
|
|
106
114
|
/** Per declaration, whether the sweep judges its liveness. */
|
|
107
115
|
readonly subject: readonly boolean[];
|
|
108
116
|
/**
|
|
@@ -135,7 +143,9 @@ export function graphOf(
|
|
|
135
143
|
references: readonly Reference[],
|
|
136
144
|
roots: readonly Root[],
|
|
137
145
|
testFiles: readonly string[],
|
|
146
|
+
supportFiles: readonly string[] = [],
|
|
138
147
|
): Graph {
|
|
148
|
+
const supports = new Set(supportFiles);
|
|
139
149
|
const index = new Map(symbols.map((symbol, at) => [symbol.id, at]));
|
|
140
150
|
const at = (id: string): number => index.get(id) ?? OUTSIDE;
|
|
141
151
|
const tests = new Set(testFiles);
|
|
@@ -187,6 +197,7 @@ export function graphOf(
|
|
|
187
197
|
made,
|
|
188
198
|
parent,
|
|
189
199
|
test: symbols.map((symbol) => tests.has(symbol.position.path)),
|
|
200
|
+
support: symbols.map((symbol) => supports.has(symbol.position.path)),
|
|
190
201
|
subject: symbols.map((symbol) => swept(symbol.kind)),
|
|
191
202
|
exportsOf,
|
|
192
203
|
rooted,
|
package/src/host.ts
CHANGED
|
@@ -27,6 +27,12 @@ export interface Host {
|
|
|
27
27
|
readDirectory(path: string): readonly DirectoryEntry[];
|
|
28
28
|
/** What one path names, `"absent"` where nothing is there. */
|
|
29
29
|
kindOf(path: string): PathKind;
|
|
30
|
+
/**
|
|
31
|
+
* The path one path names once every symbolic link along it is followed, or a throw
|
|
32
|
+
* naming why it cannot be: a package manager links an installed workspace package
|
|
33
|
+
* into `node_modules`, and the link's target is what says which package it is.
|
|
34
|
+
*/
|
|
35
|
+
realPath(path: string): string;
|
|
30
36
|
/**
|
|
31
37
|
* The version of the package this analyzer was installed from, or a throw
|
|
32
38
|
* naming why it cannot be known. It is a method rather than a value because
|
|
@@ -41,4 +47,21 @@ export interface Host {
|
|
|
41
47
|
* never a part of it.
|
|
42
48
|
*/
|
|
43
49
|
writeDocument(path: string, pieces: Iterable<string>): void;
|
|
50
|
+
/**
|
|
51
|
+
* A new empty directory of its own, outside every tree the run reads, or a throw naming
|
|
52
|
+
* why it cannot be made. The run removes it before it ends.
|
|
53
|
+
*/
|
|
54
|
+
temporaryDirectory(): TemporaryDirectory;
|
|
55
|
+
/**
|
|
56
|
+
* The command that runs this analyzer's mapper for component files, under the runtime
|
|
57
|
+
* the analyzer itself runs under. The compiler starts it to read a component file.
|
|
58
|
+
*/
|
|
59
|
+
componentMapperCommand(): readonly string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** One directory a run made for itself. */
|
|
63
|
+
export interface TemporaryDirectory {
|
|
64
|
+
readonly path: string;
|
|
65
|
+
/** Removes the directory and everything in it; removing it twice removes it once. */
|
|
66
|
+
remove(): void;
|
|
44
67
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { run, SETTING_OPTIONS, type Writer } from "./run.ts";
|
|
2
|
-
export type { DirectoryEntry, Host, PathKind } from "./host.ts";
|
|
2
|
+
export type { DirectoryEntry, Host, PathKind, TemporaryDirectory } from "./host.ts";
|
|
3
3
|
export { DECLINED_CONVENTIONS, type DeclinedConvention } from "./entry-point-gaps.ts";
|
|
4
4
|
export {
|
|
5
5
|
byPosition,
|
|
@@ -14,33 +14,16 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import {
|
|
17
|
-
isArrayLiteralExpression,
|
|
18
|
-
isArrowFunction,
|
|
19
|
-
isAsExpression,
|
|
20
|
-
isBinaryExpression,
|
|
21
|
-
isCallExpression,
|
|
22
17
|
isClassDeclaration,
|
|
23
18
|
isClassExpression,
|
|
24
19
|
isClassStaticBlockDeclaration,
|
|
25
|
-
isConditionalExpression,
|
|
26
20
|
isFunctionDeclaration,
|
|
27
21
|
isFunctionExpression,
|
|
28
22
|
isGetAccessorDeclaration,
|
|
29
23
|
isHeritageClause,
|
|
30
|
-
isIdentifier,
|
|
31
24
|
isMethodDeclaration,
|
|
32
|
-
isNewExpression,
|
|
33
25
|
isObjectLiteralExpression,
|
|
34
|
-
isParameterDeclaration,
|
|
35
|
-
isParenthesizedExpression,
|
|
36
|
-
isPropertyAssignment,
|
|
37
|
-
isPropertyDeclaration,
|
|
38
|
-
isReturnStatement,
|
|
39
|
-
isSatisfiesExpression,
|
|
40
26
|
isSetAccessorDeclaration,
|
|
41
|
-
isShorthandPropertyAssignment,
|
|
42
|
-
isSpreadElement,
|
|
43
|
-
isVariableDeclaration,
|
|
44
27
|
ModifierFlags,
|
|
45
28
|
SyntaxKind,
|
|
46
29
|
type Expression,
|
|
@@ -53,6 +36,7 @@ import type { DetectorInput, Evidence } from "./exempt.ts";
|
|
|
53
36
|
import { nodeKey } from "./inventory.ts";
|
|
54
37
|
import { renderPosition } from "./position.ts";
|
|
55
38
|
import { must } from "./query.ts";
|
|
39
|
+
import { handedOn, valuesOf } from "./value-flow.ts";
|
|
56
40
|
|
|
57
41
|
/** One class the inventory holds, by its declaration. */
|
|
58
42
|
interface ClassAt {
|
|
@@ -85,14 +69,6 @@ interface Pair {
|
|
|
85
69
|
readonly sites: Node[];
|
|
86
70
|
}
|
|
87
71
|
|
|
88
|
-
/** The operators of an assignment whose right side is stored into the left side's type. */
|
|
89
|
-
const STORES: ReadonlySet<SyntaxKind> = new Set([
|
|
90
|
-
SyntaxKind.EqualsToken,
|
|
91
|
-
SyntaxKind.QuestionQuestionEqualsToken,
|
|
92
|
-
SyntaxKind.BarBarEqualsToken,
|
|
93
|
-
SyntaxKind.AmpersandAmpersandEqualsToken,
|
|
94
|
-
]);
|
|
95
|
-
|
|
96
72
|
/** Whether one node declares a static member, whose `this` is the class itself. */
|
|
97
73
|
function isStatic(node: Node): boolean {
|
|
98
74
|
const flags = (node as { readonly modifierFlags?: number }).modifierFlags ?? 0;
|
|
@@ -109,73 +85,6 @@ function isObjectLiteralMethod(node: Node): boolean {
|
|
|
109
85
|
);
|
|
110
86
|
}
|
|
111
87
|
|
|
112
|
-
/** Whether a declaration writes its type, so its initializer is read in that type's context. */
|
|
113
|
-
function writesType(node: Node): boolean {
|
|
114
|
-
return (node as { readonly type?: Node }).type !== undefined;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* The values one written expression hands on: the expression itself, or what it
|
|
119
|
-
* evaluates to through parentheses, a conditional and the operators whose result is
|
|
120
|
-
* one of their operands.
|
|
121
|
-
*/
|
|
122
|
-
function valuesOf(expression: Expression): readonly Expression[] {
|
|
123
|
-
if (isParenthesizedExpression(expression)) {
|
|
124
|
-
return valuesOf(expression.expression);
|
|
125
|
-
}
|
|
126
|
-
if (isConditionalExpression(expression)) {
|
|
127
|
-
return [...valuesOf(expression.whenTrue), ...valuesOf(expression.whenFalse)];
|
|
128
|
-
}
|
|
129
|
-
if (isBinaryExpression(expression)) {
|
|
130
|
-
const operator = expression.operatorToken.kind;
|
|
131
|
-
if (operator === SyntaxKind.QuestionQuestionToken || operator === SyntaxKind.BarBarToken) {
|
|
132
|
-
return [...valuesOf(expression.left), ...valuesOf(expression.right)];
|
|
133
|
-
}
|
|
134
|
-
if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.CommaToken) {
|
|
135
|
-
return valuesOf(expression.right);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return [expression];
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** The expressions one node hands to a position typed by its context. */
|
|
142
|
-
function handedOn(node: Node): readonly Expression[] {
|
|
143
|
-
if (
|
|
144
|
-
(isVariableDeclaration(node) || isPropertyDeclaration(node) || isParameterDeclaration(node)) &&
|
|
145
|
-
node.initializer !== undefined &&
|
|
146
|
-
writesType(node)
|
|
147
|
-
) {
|
|
148
|
-
return [node.initializer];
|
|
149
|
-
}
|
|
150
|
-
if (isBinaryExpression(node) && STORES.has(node.operatorToken.kind)) {
|
|
151
|
-
return [node.right];
|
|
152
|
-
}
|
|
153
|
-
if (isCallExpression(node) || isNewExpression(node)) {
|
|
154
|
-
return (node.arguments ?? []).filter((argument) => !isSpreadElement(argument));
|
|
155
|
-
}
|
|
156
|
-
if (isReturnStatement(node)) {
|
|
157
|
-
return node.expression === undefined ? [] : [node.expression];
|
|
158
|
-
}
|
|
159
|
-
if (isArrowFunction(node)) {
|
|
160
|
-
return node.body.kind === SyntaxKind.Block ? [] : [node.body];
|
|
161
|
-
}
|
|
162
|
-
if (isArrayLiteralExpression(node)) {
|
|
163
|
-
return node.elements.filter(
|
|
164
|
-
(element) => !isSpreadElement(element) && element.kind !== SyntaxKind.OmittedExpression,
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
if (isPropertyAssignment(node)) {
|
|
168
|
-
return [node.initializer];
|
|
169
|
-
}
|
|
170
|
-
if (isShorthandPropertyAssignment(node)) {
|
|
171
|
-
return isIdentifier(node.name) ? [node.name] : [];
|
|
172
|
-
}
|
|
173
|
-
if (isAsExpression(node) || isSatisfiesExpression(node)) {
|
|
174
|
-
return [node.expression];
|
|
175
|
-
}
|
|
176
|
-
return [];
|
|
177
|
-
}
|
|
178
|
-
|
|
179
88
|
/** What one project's walk found to resolve. */
|
|
180
89
|
interface Walked {
|
|
181
90
|
readonly classes: readonly ClassAt[];
|
package/src/inventory.ts
CHANGED
|
@@ -31,7 +31,13 @@ import {
|
|
|
31
31
|
import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
|
|
32
32
|
import type { Host } from "./host.ts";
|
|
33
33
|
import { dirnamePath, joinPath, relativePath } from "./paths.ts";
|
|
34
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
byPosition,
|
|
36
|
+
positionKey,
|
|
37
|
+
renderPosition,
|
|
38
|
+
writtenLength,
|
|
39
|
+
type Position,
|
|
40
|
+
} from "./position.ts";
|
|
35
41
|
import { computedComponent, nameComponent, renderRef, type Component, type Module } from "./ref.ts";
|
|
36
42
|
import { isAnswered, UNANSWERED } from "./query.ts";
|
|
37
43
|
import type { Handle, ProjectView } from "./session.ts";
|
|
@@ -297,7 +303,10 @@ function typeParametersOf(node: Node): readonly Node[] {
|
|
|
297
303
|
if (
|
|
298
304
|
isFunctionDeclaration(node) ||
|
|
299
305
|
isMethodDeclaration(node) ||
|
|
300
|
-
isMethodSignatureDeclaration(node)
|
|
306
|
+
isMethodSignatureDeclaration(node) ||
|
|
307
|
+
isClassDeclaration(node) ||
|
|
308
|
+
isInterfaceDeclaration(node) ||
|
|
309
|
+
isTypeAliasDeclaration(node)
|
|
301
310
|
) {
|
|
302
311
|
return node.typeParameters ?? [];
|
|
303
312
|
}
|
|
@@ -534,7 +543,12 @@ export function inventory<Brand>(
|
|
|
534
543
|
typeParameter: undefined,
|
|
535
544
|
});
|
|
536
545
|
exportScopes.push({ owner: fileRecord, nameNode: file });
|
|
537
|
-
|
|
546
|
+
// A statement a component file's module appends is not written in the file.
|
|
547
|
+
const written = writtenLength(file);
|
|
548
|
+
walkStatements(
|
|
549
|
+
file.statements.filter((statement) => statement.end <= written),
|
|
550
|
+
fileRecord,
|
|
551
|
+
);
|
|
538
552
|
}
|
|
539
553
|
|
|
540
554
|
/** One declaration of a module, a namespace or a container body. */
|
package/src/kinds.ts
CHANGED
|
@@ -60,6 +60,8 @@ const ROWS: readonly KindRow[] = [
|
|
|
60
60
|
name: "unused-member",
|
|
61
61
|
languages: ["go", "ts"],
|
|
62
62
|
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.",
|
|
63
|
+
precondition:
|
|
64
|
+
"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.",
|
|
63
65
|
defaultSeverity: "deny",
|
|
64
66
|
maxClass: "certain",
|
|
65
67
|
fixability: "deletable",
|
|
@@ -69,7 +71,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
69
71
|
code: "DS1004",
|
|
70
72
|
name: "test-only-use",
|
|
71
73
|
languages: ["go", "ts"],
|
|
72
|
-
rule: "A symbol with zero production references and at least one test reference: an unused-exported, unused-unexported or unused-member candidate whose test reference count is not zero, reported once under this code. A reference from a consumer's test files is a test reference unless the configuration counts consumer tests as production.",
|
|
74
|
+
rule: "A symbol with zero production references and at least one test reference: an unused-exported, unused-unexported or unused-member candidate whose test reference count is not zero, reported once under this code. A reference from a consumer's test files is a test reference unless the configuration counts consumer tests as production. A reference from test-support code, a package or file that only test code imports, is a test reference, and a declaration of test-support code is never reported under this code.",
|
|
73
75
|
defaultSeverity: "deny",
|
|
74
76
|
maxClass: "certain",
|
|
75
77
|
fixability: "deletable",
|
|
@@ -101,7 +103,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
101
103
|
languages: ["go", "ts"],
|
|
102
104
|
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.",
|
|
103
105
|
precondition:
|
|
104
|
-
"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.",
|
|
106
|
+
"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.",
|
|
105
107
|
defaultSeverity: "warn",
|
|
106
108
|
maxClass: "certain",
|
|
107
109
|
fixability: "narrowable",
|
|
@@ -113,7 +115,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
113
115
|
languages: ["go"],
|
|
114
116
|
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.",
|
|
115
117
|
precondition:
|
|
116
|
-
"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.",
|
|
118
|
+
"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.",
|
|
117
119
|
defaultSeverity: "warn",
|
|
118
120
|
maxClass: "certain",
|
|
119
121
|
fixability: "narrowable",
|
|
@@ -177,7 +179,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
177
179
|
code: "DS1301",
|
|
178
180
|
name: "write-only-symbol",
|
|
179
181
|
languages: ["go", "ts"],
|
|
180
|
-
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.",
|
|
182
|
+
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.",
|
|
181
183
|
defaultSeverity: "deny",
|
|
182
184
|
maxClass: "certain",
|
|
183
185
|
fixability: "deletable",
|
|
@@ -317,7 +319,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
317
319
|
languages: ["go", "ts"],
|
|
318
320
|
rule: "A parameter with no reference inside its function body, on a function whose signature is free to change.",
|
|
319
321
|
precondition:
|
|
320
|
-
"The signature must be free, defined as follows: the function is not a method retained by interface satisfaction, is not used as a value, is not a go:linkname or cgo target, and is not a stub whose body is empty or only panics. A parameter its body never names is dead whatever the callers, so a published declaration of a library is reported too, with the fixability the vocabulary gives the kind: the signature change is a breaking change.",
|
|
322
|
+
"The signature must be free, defined as follows: the function is not a method retained by interface satisfaction, is not used as a value, is not a go:linkname or cgo target, is not a function the Go test driver runs (a test, benchmark or fuzz test of a test file, or TestMain), and is not a stub whose body is empty or only panics. A parameter its body never names is dead whatever the callers, so a published declaration of a library is reported too, with the fixability the vocabulary gives the kind: the signature change is a breaking change.",
|
|
321
323
|
defaultSeverity: "warn",
|
|
322
324
|
maxClass: "certain",
|
|
323
325
|
fixability: "manual",
|