@cplieger/deadset-ts 8.0.0-dev.1 → 8.0.0-dev.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/analysis.js +30 -6
- package/dist/src/augmentations.js +105 -12
- package/dist/src/build-scripts.js +437 -0
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/convention-rows.js +62 -2
- package/dist/src/conventions.js +15 -0
- package/dist/src/dependencies.js +89 -0
- package/dist/src/entry-points.js +9 -18
- package/dist/src/findings/completion.js +122 -49
- package/dist/src/findings/intra-function.js +4 -2
- package/dist/src/findings/reads-and-writes.js +7 -6
- package/dist/src/findings/unused-declarations.js +13 -5
- package/dist/src/findings/visibility-narrowing.js +8 -3
- package/dist/src/graph.js +5 -3
- package/dist/src/intra-function-parts.js +45 -17
- package/dist/src/kinds.js +8 -8
- package/dist/src/manifest.js +18 -0
- package/dist/src/matrix.js +76 -4
- package/dist/src/references.js +34 -3
- package/dist/src/report.js +23 -4
- package/dist/src/reporters/sarif.js +12 -8
- package/dist/src/roots.js +138 -11
- package/dist/src/serialization-contract.js +258 -8
- package/dist/src/suppress.js +1 -0
- package/dist/src/sweep.js +7 -9
- package/dist/src/test-globals.js +234 -0
- package/dist/src/test-support.js +8 -11
- package/dist/src/type-errors.js +11 -3
- package/dist/src/unseen-loads.js +107 -0
- package/dist/src/verbs/analyze.js +24 -2
- package/dist/src/yaml-document.js +355 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +48 -8
- package/src/augmentations.ts +134 -16
- package/src/build-scripts.ts +564 -0
- package/src/conformance-record.ts +1 -1
- package/src/convention-rows.ts +72 -2
- package/src/conventions.ts +16 -0
- package/src/dependencies.ts +96 -0
- package/src/entry-points.ts +10 -25
- package/src/file-facts.ts +3 -0
- package/src/finding.ts +5 -0
- package/src/findings/completion.ts +153 -56
- package/src/findings/intra-function.ts +4 -2
- package/src/findings/reads-and-writes.ts +7 -6
- package/src/findings/unused-declarations.ts +17 -5
- package/src/findings/visibility-narrowing.ts +9 -3
- package/src/graph.ts +5 -3
- package/src/intra-function-parts.ts +55 -16
- package/src/kinds.ts +8 -8
- package/src/manifest.ts +21 -3
- package/src/matrix.ts +122 -4
- package/src/references.ts +38 -2
- package/src/report.ts +56 -16
- package/src/reporters/sarif.ts +16 -8
- package/src/roots.ts +162 -12
- package/src/serialization-contract.ts +309 -7
- package/src/suppress.ts +1 -0
- package/src/sweep.ts +21 -9
- package/src/test-globals.ts +275 -0
- package/src/test-support.ts +17 -13
- package/src/type-errors.ts +14 -3
- package/src/unseen-loads.ts +145 -0
- package/src/verbs/analyze.ts +28 -2
- package/src/yaml-document.ts +377 -0
package/dist/src/entry-points.js
CHANGED
|
@@ -10,14 +10,16 @@
|
|
|
10
10
|
* value that is not a literal is not evaluated and not guessed, so a file it would
|
|
11
11
|
* have named is entered by nothing here.
|
|
12
12
|
*/
|
|
13
|
-
import { isArrayLiteralExpression, isAsExpression,
|
|
13
|
+
import { isArrayLiteralExpression, isAsExpression, isCallExpression, isExportAssignment, isIdentifier, isMetaProperty, isNewExpression, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isStringLiteral, SyntaxKind, } from "@typescript/native/ast";
|
|
14
14
|
import { SymbolFlags } from "@typescript/native/sync";
|
|
15
15
|
import { configurationFiles, filesNamedBy, moduleStrings, } from "./configuration-files.js";
|
|
16
|
+
import { buildScriptEntries } from "./build-scripts.js";
|
|
16
17
|
import { globExpression } from "./glob.js";
|
|
17
18
|
import { htmlEntries } from "./html-entries.js";
|
|
18
19
|
import { documentEntries } from "./mdx-entries.js";
|
|
19
20
|
import { dirnamePath, relativePath, resolvePath } from "./paths.js";
|
|
20
21
|
import { UNANSWERED } from "./query.js";
|
|
22
|
+
import { computedLoadDirectory, leadingLiteral } from "./unseen-loads.js";
|
|
21
23
|
import { workflowEntries } from "./workflow-steps.js";
|
|
22
24
|
/** The extensions the test runner and the browser-test runner load a configuration from. */
|
|
23
25
|
const MODULE_EXTENSIONS = ["js", "mjs", "cjs", "ts", "mts", "cts"];
|
|
@@ -210,7 +212,7 @@ function configurationEntries(host, targetRoot, files, parse, selected) {
|
|
|
210
212
|
strings(dirnamePath(document.path), unselectedIn(document.path, document.strings));
|
|
211
213
|
}
|
|
212
214
|
for (const { file } of outside) {
|
|
213
|
-
strings(dirnamePath(file.fileName),
|
|
215
|
+
strings(dirnamePath(file.fileName), unselected(file));
|
|
214
216
|
}
|
|
215
217
|
return found;
|
|
216
218
|
}
|
|
@@ -398,17 +400,6 @@ function workersOf(project, file, files) {
|
|
|
398
400
|
}
|
|
399
401
|
return found;
|
|
400
402
|
}
|
|
401
|
-
/** The literal text a computed path starts with: a template's head, or a concatenation's first literal. */
|
|
402
|
-
function leadingLiteral(node) {
|
|
403
|
-
const at = unwrapped(node);
|
|
404
|
-
if (isTemplateExpression(at)) {
|
|
405
|
-
return at.head.text;
|
|
406
|
-
}
|
|
407
|
-
if (isBinaryExpression(at) && at.operatorToken.kind === SyntaxKind.PlusToken) {
|
|
408
|
-
return literalOf(unwrapped(at.left)) ?? leadingLiteral(at.left);
|
|
409
|
-
}
|
|
410
|
-
return undefined;
|
|
411
|
-
}
|
|
412
403
|
/** Whether one call is `import.meta.glob(...)`. */
|
|
413
404
|
function isMetaGlob(node) {
|
|
414
405
|
if (!isCallExpression(node) || !isPropertyAccessExpression(node.expression)) {
|
|
@@ -432,11 +423,8 @@ function computedImportsOf(file, files) {
|
|
|
432
423
|
const dir = dirnamePath(file.fileName);
|
|
433
424
|
const found = [];
|
|
434
425
|
const below = (prefix) => {
|
|
435
|
-
const target =
|
|
436
|
-
if (
|
|
437
|
-
!prefix.endsWith("/") ||
|
|
438
|
-
target === dir ||
|
|
439
|
-
relativePath(target, dir) !== undefined) {
|
|
426
|
+
const target = computedLoadDirectory(dir, prefix);
|
|
427
|
+
if (target === undefined) {
|
|
440
428
|
return;
|
|
441
429
|
}
|
|
442
430
|
for (const candidate of files.byName.values()) {
|
|
@@ -522,6 +510,9 @@ export function entryPoints(project, files, testFiles, host, targetRoot, parse,
|
|
|
522
510
|
...(names === undefined ? {} : { names }),
|
|
523
511
|
});
|
|
524
512
|
}
|
|
513
|
+
for (const { file, source } of buildScriptEntries(host, targetRoot, files, parse)) {
|
|
514
|
+
found.push({ file, rule: "bundler-entry", source, exports: true });
|
|
515
|
+
}
|
|
525
516
|
for (const { file, source } of workflowEntries(host, targetRoot, files)) {
|
|
526
517
|
found.push({ file, rule: "workflow-step", source, exports: true });
|
|
527
518
|
}
|
|
@@ -105,6 +105,32 @@ function reachabilityClasses(input) {
|
|
|
105
105
|
return published.has(id) ? outside : "certain";
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Whether one test of dead code references a target symbol that is published API of a run
|
|
110
|
+
* that declares no consumer, which outside programs the run cannot see may use, so the
|
|
111
|
+
* target counts as live for the test and the test is no test of dead code.
|
|
112
|
+
*/
|
|
113
|
+
export function testsUnseenAPI(input) {
|
|
114
|
+
if (input.boundary.consumers.declared.length > 0) {
|
|
115
|
+
return () => false;
|
|
116
|
+
}
|
|
117
|
+
const union = input.swept.matrix.union;
|
|
118
|
+
const published = publishedIds(union);
|
|
119
|
+
return (id) => testTargets(union, id).some((target) => published.has(target));
|
|
120
|
+
}
|
|
121
|
+
/** The target symbols one test of dead code references, by id: production declarations the sweep judges. */
|
|
122
|
+
function testTargets(union, id) {
|
|
123
|
+
return (union.out[union.at(id)] ?? []).flatMap((edge) => {
|
|
124
|
+
const target = union.symbols[edge.to];
|
|
125
|
+
const testCode = union.test[edge.to] === true || union.support[edge.to] === true;
|
|
126
|
+
return target !== undefined &&
|
|
127
|
+
!testCode &&
|
|
128
|
+
union.ambient[edge.to] !== true &&
|
|
129
|
+
union.subject[edge.to] === true
|
|
130
|
+
? [target.id]
|
|
131
|
+
: [];
|
|
132
|
+
});
|
|
133
|
+
}
|
|
108
134
|
/**
|
|
109
135
|
* The confidence ceiling of a test of dead code: the lowest reachability class among the
|
|
110
136
|
* target symbols it references, because its deadness is theirs, whether or not they share
|
|
@@ -113,33 +139,41 @@ function reachabilityClasses(input) {
|
|
|
113
139
|
function testCeilings(input, classOf) {
|
|
114
140
|
const union = input.swept.matrix.union;
|
|
115
141
|
const tests = new Set(input.swept.sweep.candidates.filter((one) => one.testOfDeadCode).map((one) => one.id));
|
|
116
|
-
return (id) =>
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
142
|
+
return (id) => tests.has(id)
|
|
143
|
+
? testTargets(union, id).reduce((lowest, target) => capped(lowest, classOf(target)), "certain")
|
|
144
|
+
: "certain";
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The subjects dead only because production code only tests use is: each declaration the
|
|
148
|
+
* sweep marks test-used, each `DS1502` file of such code, and each test of dead code every
|
|
149
|
+
* target of which is one. Nothing they hold references a dead declaration outside them, so
|
|
150
|
+
* the rest of a component they share is reported without them.
|
|
151
|
+
*/
|
|
152
|
+
function testUsedSubjects(input) {
|
|
153
|
+
const { matrix, sweep } = input.swept;
|
|
154
|
+
const union = matrix.union;
|
|
155
|
+
const used = new Set(sweep.candidates.filter((one) => one.testUsed).map((one) => one.id));
|
|
156
|
+
for (const test of sweep.candidates.filter((one) => one.testOfDeadCode)) {
|
|
157
|
+
const targets = testTargets(union, test.id);
|
|
158
|
+
if (targets.length > 0 && targets.every((target) => used.has(target))) {
|
|
159
|
+
used.add(test.id);
|
|
130
160
|
}
|
|
131
|
-
|
|
132
|
-
|
|
161
|
+
}
|
|
162
|
+
const files = new Set(union.symbols.flatMap((symbol, at) => matrix.testUsed[at] === true ? [symbol.position.path] : []));
|
|
163
|
+
return { declarations: used, files };
|
|
133
164
|
}
|
|
134
165
|
/**
|
|
135
166
|
* The ceiling the roots the run found put on one dead subject: a subject other declarations
|
|
136
|
-
* reference, found dead only because no root reaches them, is `probable` unless
|
|
137
|
-
*
|
|
138
|
-
*
|
|
167
|
+
* reference, found dead only because no root reaches them, is `probable` unless the
|
|
168
|
+
* configuration declares the roots complete and no configuration holding the subject loads
|
|
169
|
+
* a module the analysis cannot resolve, because a root the analysis cannot see would make
|
|
170
|
+
* it live; every other declaration has none below `certain`.
|
|
139
171
|
*/
|
|
140
|
-
function rootsCeiling(input, finding, candidate) {
|
|
172
|
+
function rootsCeiling(input, finding, candidate, configurations) {
|
|
141
173
|
const unreachable = candidate?.relation === "reachability" || finding.unreachableOnly === true;
|
|
142
|
-
|
|
174
|
+
const unseen = new Set((input.files.unseenLoads ?? []).map((load) => load.configuration));
|
|
175
|
+
const complete = input.config.ts.rootsComplete && !configurations.some((one) => unseen.has(one));
|
|
176
|
+
return unreachable && !complete ? "probable" : "certain";
|
|
143
177
|
}
|
|
144
178
|
/**
|
|
145
179
|
* The component each dead declaration of the run falls in, and none for a live one, with
|
|
@@ -152,8 +186,24 @@ function findingComponents(swept, components, cascade) {
|
|
|
152
186
|
held.set(member, component);
|
|
153
187
|
}
|
|
154
188
|
}
|
|
189
|
+
const listed = membersListed(swept);
|
|
190
|
+
return (id) => {
|
|
191
|
+
const component = held.get(id);
|
|
192
|
+
return component === undefined
|
|
193
|
+
? undefined
|
|
194
|
+
: {
|
|
195
|
+
id: component.id,
|
|
196
|
+
root: component.roots.includes(id),
|
|
197
|
+
symbolCount: component.members.length,
|
|
198
|
+
deletableLines: component.deletableLines,
|
|
199
|
+
...(cascade === "full" ? { members: listed(component.members) } : {}),
|
|
200
|
+
};
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/** The members of a component as the run lists them, positioned and in the canonical order. */
|
|
204
|
+
function membersListed(swept) {
|
|
155
205
|
const symbols = new Map(swept.matrix.union.symbols.map((symbol) => [symbol.id, symbol]));
|
|
156
|
-
|
|
206
|
+
return (members) => members
|
|
157
207
|
.flatMap((member) => {
|
|
158
208
|
const symbol = symbols.get(member);
|
|
159
209
|
return symbol === undefined
|
|
@@ -170,18 +220,6 @@ function findingComponents(swept, components, cascade) {
|
|
|
170
220
|
];
|
|
171
221
|
})
|
|
172
222
|
.sort((a, b) => byPosition(a.position, b.position));
|
|
173
|
-
return (id) => {
|
|
174
|
-
const component = held.get(id);
|
|
175
|
-
return component === undefined
|
|
176
|
-
? undefined
|
|
177
|
-
: {
|
|
178
|
-
id: component.id,
|
|
179
|
-
root: component.roots.includes(id),
|
|
180
|
-
symbolCount: component.members.length,
|
|
181
|
-
deletableLines: component.deletableLines,
|
|
182
|
-
...(cascade === "full" ? { members: listed(component) } : {}),
|
|
183
|
-
};
|
|
184
|
-
};
|
|
185
223
|
}
|
|
186
224
|
/**
|
|
187
225
|
* The run's dead components with each file a `DS1502` finding reports dead among them, so
|
|
@@ -251,6 +289,7 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
251
289
|
const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
|
|
252
290
|
const classOf = reachabilityClasses(input);
|
|
253
291
|
const ceilingOf = testCeilings(input, classOf);
|
|
292
|
+
const testUsed = testUsedSubjects(input);
|
|
254
293
|
const { components, computed, files } = runComponents(swept, findings);
|
|
255
294
|
const componentOf = findingComponents(swept, components, input.config.reporters.cascade);
|
|
256
295
|
const held = new Set(components.filter((one) => one.held).flatMap((one) => one.members));
|
|
@@ -258,11 +297,12 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
258
297
|
let minted = computed;
|
|
259
298
|
const dead = new Set(components.map((component) => component.id));
|
|
260
299
|
const fallingWith = new Map();
|
|
261
|
-
for (const { heldByTests } of swept.sweep.candidates) {
|
|
300
|
+
for (const { id, heldByTests } of swept.sweep.candidates) {
|
|
262
301
|
if (heldByTests !== undefined) {
|
|
263
|
-
fallingWith.set(heldByTests, (fallingWith.get(heldByTests) ??
|
|
302
|
+
fallingWith.set(heldByTests, [...(fallingWith.get(heldByTests) ?? []), id]);
|
|
264
303
|
}
|
|
265
304
|
}
|
|
305
|
+
const listed = membersListed(swept);
|
|
266
306
|
const done = findings.map((finding) => {
|
|
267
307
|
const row = kinds.get(finding.code);
|
|
268
308
|
if (row === undefined) {
|
|
@@ -307,17 +347,40 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
307
347
|
let component = found;
|
|
308
348
|
if (component === undefined) {
|
|
309
349
|
minted += 1;
|
|
350
|
+
const falling = symbol?.kind === "interface" ? fallingWith.get(symbol.id) : undefined;
|
|
310
351
|
component = {
|
|
311
352
|
id: componentId(minted),
|
|
312
353
|
root: true,
|
|
313
|
-
symbolCount:
|
|
354
|
+
symbolCount: falling?.length ?? 1,
|
|
314
355
|
deletableLines: 0,
|
|
356
|
+
...(input.config.reporters.cascade !== "full"
|
|
357
|
+
? {}
|
|
358
|
+
: falling === undefined
|
|
359
|
+
? {
|
|
360
|
+
members: [
|
|
361
|
+
{
|
|
362
|
+
ref: finding.symbol.ref,
|
|
363
|
+
name: finding.symbol.name,
|
|
364
|
+
position: finding.position,
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
}
|
|
368
|
+
: { members: listed(falling) }),
|
|
315
369
|
};
|
|
316
370
|
}
|
|
317
371
|
const counts = candidate === undefined
|
|
318
372
|
? union.made[at]
|
|
319
373
|
: { production: candidate.productionRefs, test: candidate.testRefs };
|
|
320
|
-
const
|
|
374
|
+
const ownClass = judged === undefined ? (finding.reachabilityClass ?? "certain") : classOf(judged.id);
|
|
375
|
+
const used = judged !== undefined
|
|
376
|
+
? candidate !== undefined && testUsed.declarations.has(judged.id)
|
|
377
|
+
: finding.code === FILE_NEVER_IMPORTED && testUsed.files.has(finding.position.path);
|
|
378
|
+
const reachabilityClass = used && candidate?.testOfDeadCode !== true ? "possible" : ownClass;
|
|
379
|
+
const configurations = candidate?.configurations ??
|
|
380
|
+
(judged === undefined && !part
|
|
381
|
+
? swept.matrix.configurations
|
|
382
|
+
: (swept.matrix.heldIn[at] ?? swept.matrix.configurations));
|
|
383
|
+
const untested = capped(capped(capped(ownClass, row.maxClass), rootsCeiling(input, finding, candidate, configurations)), judged === undefined ? "certain" : ceilingOf(judged.id));
|
|
321
384
|
const generated = input.files.generated?.has(finding.position.path) === true;
|
|
322
385
|
const fixability = generated
|
|
323
386
|
? "none"
|
|
@@ -341,16 +404,14 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
341
404
|
position: finding.position,
|
|
342
405
|
symbol: finding.symbol,
|
|
343
406
|
reachabilityClass,
|
|
344
|
-
confidence:
|
|
407
|
+
confidence: used ? "possible" : untested,
|
|
408
|
+
...(used ? { componentConfidence: untested } : {}),
|
|
345
409
|
...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
|
|
346
410
|
testOnly: judged !== undefined && counts?.production === 0 && counts.test > 0,
|
|
347
411
|
generated,
|
|
348
412
|
component,
|
|
349
413
|
retainedBy: [],
|
|
350
|
-
configurations
|
|
351
|
-
(judged === undefined && !part
|
|
352
|
-
? swept.matrix.configurations
|
|
353
|
-
: (swept.matrix.heldIn[at] ?? swept.matrix.configurations)),
|
|
414
|
+
configurations,
|
|
354
415
|
consumersLoaded: input.boundary.consumers.loaded,
|
|
355
416
|
fixability,
|
|
356
417
|
severity: severityOf(input, finding.code, row.defaultSeverity),
|
|
@@ -365,7 +426,7 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
365
426
|
/**
|
|
366
427
|
* The findings with every finding of a dead component capped by the lowest confidence
|
|
367
428
|
* among the component's root members, so the minimum confidence reports or withholds a
|
|
368
|
-
* component whole.
|
|
429
|
+
* component whole but for the findings test-used code lowers on their own.
|
|
369
430
|
*/
|
|
370
431
|
function componentCapped(findings, dead) {
|
|
371
432
|
const lowest = new Map();
|
|
@@ -373,16 +434,27 @@ function componentCapped(findings, dead) {
|
|
|
373
434
|
const { id, root } = finding.component;
|
|
374
435
|
if (root && dead.has(id)) {
|
|
375
436
|
const held = lowest.get(id);
|
|
376
|
-
|
|
437
|
+
const own = componentConfidenceOf(finding);
|
|
438
|
+
lowest.set(id, held === undefined ? own : capped(held, own));
|
|
377
439
|
}
|
|
378
440
|
}
|
|
379
441
|
return findings.map((finding) => {
|
|
380
442
|
const cap = lowest.get(finding.component.id);
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
443
|
+
if (cap === undefined) {
|
|
444
|
+
return finding;
|
|
445
|
+
}
|
|
446
|
+
const own = finding.componentConfidence;
|
|
447
|
+
return {
|
|
448
|
+
...finding,
|
|
449
|
+
confidence: capped(finding.confidence, cap),
|
|
450
|
+
...(own === undefined ? {} : { componentConfidence: capped(own, cap) }),
|
|
451
|
+
};
|
|
384
452
|
});
|
|
385
453
|
}
|
|
454
|
+
/** The confidence a finding gives its component's cap and the dials. */
|
|
455
|
+
function componentConfidenceOf(finding) {
|
|
456
|
+
return finding.componentConfidence ?? finding.confidence;
|
|
457
|
+
}
|
|
386
458
|
/** Whether the dials withhold a finding by its own severity or confidence. */
|
|
387
459
|
function dialed(finding, minConfidence) {
|
|
388
460
|
return finding.severity === "allow" || RANK[finding.confidence] < RANK[minConfidence];
|
|
@@ -397,7 +469,8 @@ function dialed(finding, minConfidence) {
|
|
|
397
469
|
export function dialsOf(input, findings) {
|
|
398
470
|
const minConfidence = input.config.analysis.minConfidence;
|
|
399
471
|
const components = new Set(findings
|
|
400
|
-
.filter((finding) => finding.component.root &&
|
|
472
|
+
.filter((finding) => finding.component.root &&
|
|
473
|
+
dialed({ ...finding, confidence: componentConfidenceOf(finding) }, minConfidence))
|
|
401
474
|
.map((finding) => finding.component.id));
|
|
402
475
|
return {
|
|
403
476
|
allows: (code) => {
|
|
@@ -28,7 +28,8 @@ function overlapClause(overlap, part) {
|
|
|
28
28
|
* caller may pass a function used as a value more arguments than it declares, so there one
|
|
29
29
|
* a read parameter follows is renamed to an underscore name, which the compilers read as
|
|
30
30
|
* declared unread, and one no read parameter follows is deleted: kept where the call's
|
|
31
|
-
* inference may read its annotation, at `probable` where nothing reads the annotation
|
|
31
|
+
* inference may read its annotation, at `probable` where nothing reads the annotation or
|
|
32
|
+
* where it is a name a contextually typed object pattern binds.
|
|
32
33
|
*/
|
|
33
34
|
function parameterVerdict(part, free, freeButValued) {
|
|
34
35
|
const place = part.parameter;
|
|
@@ -45,7 +46,8 @@ function parameterVerdict(part, free, freeButValued) {
|
|
|
45
46
|
if (place?.annotated === "steers") {
|
|
46
47
|
return undefined;
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
+
const unseen = place?.annotated === "free" || place?.contextualPattern === true;
|
|
50
|
+
return { fix: "delete", confidence: unseen ? "probable" : "certain" };
|
|
49
51
|
}
|
|
50
52
|
/**
|
|
51
53
|
* Whether one part is reported. A parameter needs its function's signature free to
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { positionKey } from "../position.js";
|
|
2
2
|
import { declarationKey } from "../ref.js";
|
|
3
|
-
/** The containers whose type parameters `DS1303` reports: a function and a
|
|
4
|
-
const SIGNATURES = new Set(["function", "method"]);
|
|
3
|
+
/** The containers whose type parameters `DS1303` reports: a function, a class method and a type alias. */
|
|
4
|
+
const SIGNATURES = new Set(["function", "method", "type"]);
|
|
5
5
|
function indexed(input) {
|
|
6
6
|
const { matrix, sweep } = input.swept;
|
|
7
7
|
return {
|
|
@@ -165,10 +165,11 @@ function unusedEnumMembers(run) {
|
|
|
165
165
|
return unnamed(run, "enum-member").map((symbol) => findingAbout(symbol, "DS1302", `enum member ${symbol.name} is named nowhere`));
|
|
166
166
|
}
|
|
167
167
|
/**
|
|
168
|
-
* `DS1303`: a type parameter of a function
|
|
169
|
-
* declaration's signature nor its body names.
|
|
170
|
-
* a
|
|
171
|
-
*
|
|
168
|
+
* `DS1303`: a type parameter of a function, a class method or a type alias that neither
|
|
169
|
+
* the declaration's signature nor its body names. An alias names no type of its own, so
|
|
170
|
+
* a parameter its type never names makes no two instantiations distinct. One of a class,
|
|
171
|
+
* an interface or a method signature is never reported: a class or an interface merges
|
|
172
|
+
* only with declarations of the same parameters.
|
|
172
173
|
*/
|
|
173
174
|
function unusedTypeParameters(run) {
|
|
174
175
|
return unnamed(run, "type-parameter")
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { testsUnseenAPI } from "./completion.js";
|
|
1
2
|
import { unreachableExports } from "./visibility-narrowing.js";
|
|
2
3
|
/** The kinds of declaration that are members of a class, an interface, a type or an enum. */
|
|
3
4
|
const MEMBERS = new Set([
|
|
@@ -41,13 +42,14 @@ const TEST_OF_DEAD_CODE = "every target symbol this test references is reported
|
|
|
41
42
|
* The code one candidate is reported under, or none, each symbol once under the most
|
|
42
43
|
* specific code: a test of dead code first; no other test-file declaration or enum
|
|
43
44
|
* member nothing names; a test-support declaration test code references as test-only
|
|
44
|
-
* use; then deprecation, a test reference,
|
|
45
|
-
* unused export being the
|
|
45
|
+
* use; then deprecation, a test reference, or only dead production references beside a
|
|
46
|
+
* test that stays, and the unreferenced codes, an unimportable unused export being the
|
|
47
|
+
* unreachable kind's.
|
|
46
48
|
*/
|
|
47
49
|
function codeOf(judged) {
|
|
48
50
|
const { candidate, symbol } = judged;
|
|
49
51
|
if (candidate.testOfDeadCode) {
|
|
50
|
-
return "DS1005";
|
|
52
|
+
return judged.testsUnseenAPI ? undefined : "DS1005";
|
|
51
53
|
}
|
|
52
54
|
if ((judged.test && !candidate.unreferencedTest) || NOT_THIS_FAMILY.has(symbol.kind)) {
|
|
53
55
|
return undefined;
|
|
@@ -65,7 +67,9 @@ function codeOf(judged) {
|
|
|
65
67
|
if (candidate.productionRefs === 0 && judged.deprecated) {
|
|
66
68
|
return "DS1006";
|
|
67
69
|
}
|
|
68
|
-
if (candidate.
|
|
70
|
+
if (candidate.testRefs > 0 &&
|
|
71
|
+
(candidate.productionRefs === 0 ||
|
|
72
|
+
(candidate.relation === "reachability" && candidate.keptByTests))) {
|
|
69
73
|
return "DS1004";
|
|
70
74
|
}
|
|
71
75
|
if (MEMBERS.has(symbol.kind)) {
|
|
@@ -109,7 +113,9 @@ function messageOf(code, judged) {
|
|
|
109
113
|
case "DS1006":
|
|
110
114
|
return `deprecated ${word} has no production reference`;
|
|
111
115
|
case "DS1004":
|
|
112
|
-
return
|
|
116
|
+
return candidate.productionRefs === 0
|
|
117
|
+
? `${word} is referenced only from test files and never from production code`
|
|
118
|
+
: `${word} is referenced only from test files and from declarations that are themselves dead`;
|
|
113
119
|
}
|
|
114
120
|
const subject = code === "DS1003"
|
|
115
121
|
? `${visibilityWord(symbol)}${word}`
|
|
@@ -194,6 +200,7 @@ function familyFindings(input, fallen) {
|
|
|
194
200
|
const union = input.swept.matrix.union;
|
|
195
201
|
const dead = new Set(input.swept.sweep.candidates.map((candidate) => candidate.id));
|
|
196
202
|
const unreachable = unreachableExports(input);
|
|
203
|
+
const unseenAPI = testsUnseenAPI(input);
|
|
197
204
|
let brands;
|
|
198
205
|
const found = [];
|
|
199
206
|
for (const candidate of input.swept.sweep.candidates) {
|
|
@@ -212,6 +219,7 @@ function familyFindings(input, fallen) {
|
|
|
212
219
|
test: union.test[at] === true,
|
|
213
220
|
deprecated: input.deprecated.has(symbol.id),
|
|
214
221
|
unreachable: unreachable.has(symbol.id),
|
|
222
|
+
testsUnseenAPI: candidate.testOfDeadCode && unseenAPI(candidate.id),
|
|
215
223
|
};
|
|
216
224
|
const code = codeOf(judged);
|
|
217
225
|
if (code === undefined ||
|
|
@@ -34,12 +34,17 @@ const RUN_ONLY = new Set(["configuration-string", "script"]);
|
|
|
34
34
|
function entersByName(kinds) {
|
|
35
35
|
return [...(kinds ?? [])].some((kind) => !PUBLISHING.has(kind) && !RUN_ONLY.has(kind));
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* The root kinds whose root of a file enters no export by name: a document's import roots
|
|
39
|
+
* each export it imports, so an export it does not import is judged as in any file.
|
|
40
|
+
*/
|
|
41
|
+
const PER_NAME = new Set(["document-import"]);
|
|
37
42
|
/**
|
|
38
43
|
* Whether the roots of a declaration's file enter it by name: a test runner's root
|
|
39
44
|
* enters only an export the runner calls, so another export of a test file is judged.
|
|
40
45
|
*/
|
|
41
46
|
function fileEntersByName(kinds, symbol) {
|
|
42
|
-
const entering = [...(kinds ?? [])].filter((kind) => !PUBLISHING.has(kind) && !RUN_ONLY.has(kind));
|
|
47
|
+
const entering = [...(kinds ?? [])].filter((kind) => !PUBLISHING.has(kind) && !RUN_ONLY.has(kind) && !PER_NAME.has(kind));
|
|
43
48
|
return (entering.some((kind) => kind !== "test-runner") || (entering.length > 0 && runnerCalls(symbol)));
|
|
44
49
|
}
|
|
45
50
|
/**
|
|
@@ -163,7 +168,7 @@ function narrowing(facts, at, symbol, world) {
|
|
|
163
168
|
* export no code outside the target can import, because the manifest declares
|
|
164
169
|
* `exports` and no root reaches it. Each arm yields to a more specific kind, so one
|
|
165
170
|
* declaration is reported once: a test whose every subject is dead, an interface, a
|
|
166
|
-
* declaration only a test file references, and a deprecated one.
|
|
171
|
+
* declaration only a test file references or a test that stays keeps, and a deprecated one.
|
|
167
172
|
*/
|
|
168
173
|
function unreachable(facts, at, symbol, candidate, input) {
|
|
169
174
|
if (!input.boundary.encapsulated ||
|
|
@@ -171,7 +176,7 @@ function unreachable(facts, at, symbol, candidate, input) {
|
|
|
171
176
|
(facts.rootedBy[at]?.size ?? 0) > 0 ||
|
|
172
177
|
candidate.testOfDeadCode ||
|
|
173
178
|
symbol.kind === "interface" ||
|
|
174
|
-
(candidate.
|
|
179
|
+
(candidate.testRefs > 0 && (candidate.productionRefs === 0 || candidate.keptByTests)) ||
|
|
175
180
|
(candidate.productionRefs === 0 && input.deprecated.has(symbol.id))) {
|
|
176
181
|
return undefined;
|
|
177
182
|
}
|
package/dist/src/graph.js
CHANGED
|
@@ -34,9 +34,9 @@ const SWEPT = {
|
|
|
34
34
|
* Whether a root of each kind names a caller, which holds it live under both relations: a
|
|
35
35
|
* tool reads what it loads and what its strings name, a runtime runs a test, setup or
|
|
36
36
|
* worker file, a page's module script, a command, a script or a workflow step, the manifest
|
|
37
|
-
* names what enters the package, an alias global stands for its export,
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* names what enters the package, an alias global stands for its export, an outside
|
|
38
|
+
* declaration names a global interface merging into it, and an entry file, a configured root
|
|
39
|
+
* and a pattern assert a caller. A published API supposes a consumer: reachability alone.
|
|
40
40
|
*/
|
|
41
41
|
const NAMES_A_CALLER = {
|
|
42
42
|
"entry-file": true,
|
|
@@ -49,8 +49,10 @@ const NAMES_A_CALLER = {
|
|
|
49
49
|
pattern: true,
|
|
50
50
|
"type-error": true,
|
|
51
51
|
"type-query-alias": true,
|
|
52
|
+
"global-augmentation": true,
|
|
52
53
|
"configuration-file": true,
|
|
53
54
|
"configuration-string": true,
|
|
55
|
+
"bundler-entry": true,
|
|
54
56
|
"test-runner": true,
|
|
55
57
|
"browser-tests": true,
|
|
56
58
|
worker: true,
|
|
@@ -772,10 +772,9 @@ function calleeSteers(project, signature, at) {
|
|
|
772
772
|
}
|
|
773
773
|
/**
|
|
774
774
|
* The parts with each name of `contextual` judged where the compiler gives its function a
|
|
775
|
-
* contextual type:
|
|
776
|
-
*
|
|
777
|
-
*
|
|
778
|
-
* name read.
|
|
775
|
+
* contextual type: a name an object pattern binds is marked as one, and an annotated
|
|
776
|
+
* parameter with whether the call's inference may read its annotation. A question the
|
|
777
|
+
* checker leaves unanswered keeps the name read.
|
|
779
778
|
*/
|
|
780
779
|
function withContextualRead(project, parts, contextual) {
|
|
781
780
|
if (contextual.length === 0) {
|
|
@@ -791,15 +790,17 @@ function withContextualRead(project, parts, contextual) {
|
|
|
791
790
|
continue;
|
|
792
791
|
}
|
|
793
792
|
const place = one.part.parameter;
|
|
794
|
-
judged.set(one.part,
|
|
793
|
+
judged.set(one.part, answer === UNANSWERED || place === undefined
|
|
795
794
|
? { ...one.part, used: true }
|
|
796
|
-
:
|
|
797
|
-
...one.part,
|
|
798
|
-
|
|
799
|
-
...
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
795
|
+
: one.pattern
|
|
796
|
+
? { ...one.part, parameter: { ...place, contextualPattern: true } }
|
|
797
|
+
: {
|
|
798
|
+
...one.part,
|
|
799
|
+
parameter: {
|
|
800
|
+
...place,
|
|
801
|
+
annotated: calleeSteers(project, one.signature, one.at) ? "steers" : "free",
|
|
802
|
+
},
|
|
803
|
+
});
|
|
803
804
|
}
|
|
804
805
|
return parts.map((part) => judged.get(part) ?? part);
|
|
805
806
|
}
|
|
@@ -884,15 +885,28 @@ function functionOf(block) {
|
|
|
884
885
|
}
|
|
885
886
|
}
|
|
886
887
|
/**
|
|
887
|
-
* The locals one block declares with `let`, `const
|
|
888
|
-
* block that spell it. A `var` of a nested
|
|
889
|
-
*
|
|
888
|
+
* The locals one block declares with `let`, `const`, `var` or a function declaration
|
|
889
|
+
* inside a function, each with the names of the block that spell it. A `var` of a nested
|
|
890
|
+
* block is one where no nested function of its function names it, since the function scope
|
|
891
|
+
* holds it past the block's end.
|
|
890
892
|
*/
|
|
891
893
|
function localsOf(block) {
|
|
892
894
|
const declared = [];
|
|
893
895
|
const scope = functionOf(block);
|
|
894
896
|
const outlives = scope?.body !== block;
|
|
895
897
|
block.statements.forEach((statement, at) => {
|
|
898
|
+
if (isFunctionDeclaration(statement)) {
|
|
899
|
+
if (scope !== undefined && statement.name !== undefined && statement.body !== undefined) {
|
|
900
|
+
declared.push({
|
|
901
|
+
name: statement.name,
|
|
902
|
+
statement: at,
|
|
903
|
+
initialized: true,
|
|
904
|
+
outlives: false,
|
|
905
|
+
hoisted: true,
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
896
910
|
if (!isVariableStatement(statement)) {
|
|
897
911
|
return;
|
|
898
912
|
}
|
|
@@ -902,6 +916,11 @@ function localsOf(block) {
|
|
|
902
916
|
return;
|
|
903
917
|
}
|
|
904
918
|
for (const declaration of list.declarations) {
|
|
919
|
+
// A write to a local typed `never` is the compiler's exhaustiveness check, which
|
|
920
|
+
// fails to compile once a case goes unhandled.
|
|
921
|
+
if (declaration.type?.kind === SyntaxKind.NeverKeyword) {
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
905
924
|
for (const name of boundNames(declaration.name)) {
|
|
906
925
|
if (!blockScoped &&
|
|
907
926
|
outlives &&
|
|
@@ -913,6 +932,7 @@ function localsOf(block) {
|
|
|
913
932
|
statement: at,
|
|
914
933
|
initialized: declaration.initializer !== undefined,
|
|
915
934
|
outlives: !blockScoped && outlives,
|
|
935
|
+
hoisted: false,
|
|
916
936
|
});
|
|
917
937
|
}
|
|
918
938
|
}
|
|
@@ -953,12 +973,20 @@ function deadStores(file, root, block, holder, locals, ids) {
|
|
|
953
973
|
used: false,
|
|
954
974
|
});
|
|
955
975
|
for (const local of locals) {
|
|
956
|
-
const
|
|
976
|
+
const own = local.hoisted ? block.statements[local.statement] : undefined;
|
|
977
|
+
// A function's reads of itself keep it no more than its absence would.
|
|
978
|
+
const uses = local.uses.filter((use) => readsBinding(use, local.name, ids) && (own === undefined || !within(use, own)));
|
|
957
979
|
if (!local.outlives && uses.every(isPlainWrite)) {
|
|
958
|
-
|
|
980
|
+
// A declaration with no initializer writes `undefined`, the one store left to name.
|
|
981
|
+
const writes = [...(local.initialized ? [local.name] : []), ...uses];
|
|
982
|
+
const targets = writes.length === 0 ? [local.name] : writes;
|
|
959
983
|
found.push(...targets.map((target) => deadAt(local, target)));
|
|
960
984
|
continue;
|
|
961
985
|
}
|
|
986
|
+
// A read anywhere in the block reads a function declaration, which is written first.
|
|
987
|
+
if (local.hoisted) {
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
962
990
|
if (uses.some((use) => inNestedFunction(use, block))) {
|
|
963
991
|
continue;
|
|
964
992
|
}
|