@cplieger/deadset-ts 7.2.0-dev.3 → 8.0.0-dev.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/dist/bin/mapper-connection.js +7 -4
- package/dist/src/analysis.js +116 -16
- package/dist/src/augmentations.js +21 -8
- package/dist/src/automatic-types.js +189 -0
- package/dist/src/base-url.js +18 -0
- package/dist/src/component-files.js +96 -31
- package/dist/src/components.js +7 -6
- package/dist/src/config.js +1 -0
- package/dist/src/configuration-files.js +54 -15
- package/dist/src/configured-declarations.js +24 -2
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/contextual-uses.js +26 -26
- package/dist/src/convention-rows.js +75 -0
- package/dist/src/conventions.js +101 -5
- package/dist/src/decorator.js +21 -18
- package/dist/src/dependencies.js +185 -30
- package/dist/src/destructuring.js +77 -2
- package/dist/src/discover.js +188 -85
- package/dist/src/edges.js +1 -1
- package/dist/src/entry-point-gaps.js +7 -10
- package/dist/src/entry-points.js +112 -11
- package/dist/src/findings/completion.js +116 -21
- package/dist/src/findings/dependencies-and-module-machinery.js +71 -25
- package/dist/src/findings/emitters.js +1 -1
- package/dist/src/findings/intra-function.js +44 -13
- package/dist/src/findings/non-code-artifacts.js +66 -33
- package/dist/src/findings/unused-declarations.js +21 -1
- package/dist/src/findings/visibility-narrowing.js +22 -3
- package/dist/src/generated-file.js +11 -0
- package/dist/src/git-ignore.js +86 -0
- package/dist/src/graph.js +4 -1
- package/dist/src/import-bindings.js +97 -0
- package/dist/src/indexed-access.js +130 -28
- package/dist/src/interface-satisfaction.js +30 -12
- package/dist/src/intra-function-parts.js +313 -50
- package/dist/src/inventory.js +63 -8
- package/dist/src/json-document.js +5 -5
- package/dist/src/kinds.js +13 -13
- package/dist/src/matrix.js +18 -9
- package/dist/src/mdx-entries.js +197 -0
- package/dist/src/module-declarations.js +9 -13
- package/dist/src/namespace-objects.js +28 -18
- package/dist/src/overrides.js +45 -9
- package/dist/src/paths.js +9 -0
- package/dist/src/print.js +1 -0
- package/dist/src/ref.js +10 -0
- package/dist/src/references.js +29 -13
- package/dist/src/reflective-lookup.js +8 -6
- package/dist/src/report.js +4 -2
- package/dist/src/reporters/template.js +22 -13
- package/dist/src/resolve.js +2 -0
- package/dist/src/roots.js +110 -38
- package/dist/src/schema.js +1 -0
- package/dist/src/serialization-contract.js +97 -20
- package/dist/src/session.js +26 -1
- package/dist/src/source-files.js +39 -14
- package/dist/src/spelled-names.js +58 -0
- package/dist/src/suppress-file.js +12 -4
- package/dist/src/suppress-inline.js +21 -3
- package/dist/src/suppress.js +7 -5
- package/dist/src/sweep.js +27 -10
- package/dist/src/symbol-keys.js +103 -2
- package/dist/src/template-field.js +9 -6
- package/dist/src/test-support.js +121 -17
- package/dist/src/type-errors.js +116 -35
- package/dist/src/type-parameter-reads.js +172 -0
- package/dist/src/value-reach.js +17 -5
- package/dist/src/verbs/analyze.js +19 -1
- package/dist/src/verbs/explain.js +4 -2
- package/dist/src/version-range.js +10 -5
- package/dist/src/version.js +1 -1
- package/dist/src/workflow-steps.js +119 -22
- package/dist/src/workspace-programs.js +7 -3
- package/dist/src/workspace-resolution.js +72 -13
- package/dist/src/workspace.js +1 -1
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +175 -16
- package/src/augmentations.ts +21 -10
- package/src/automatic-types.ts +246 -0
- package/src/base-url.ts +24 -0
- package/src/component-files.ts +116 -31
- package/src/components.ts +7 -6
- package/src/config.ts +3 -0
- package/src/configuration-files.ts +80 -18
- package/src/configured-declarations.ts +33 -4
- package/src/conformance-record.ts +2 -2
- package/src/contextual-uses.ts +27 -27
- package/src/convention-rows.ts +103 -0
- package/src/conventions.ts +121 -7
- package/src/decorator.ts +28 -18
- package/src/dependencies.ts +249 -43
- package/src/destructuring.ts +93 -2
- package/src/discover.ts +234 -86
- package/src/edges.ts +1 -1
- package/src/entry-point-gaps.ts +8 -11
- package/src/entry-points.ts +133 -11
- package/src/finding.ts +10 -0
- package/src/findings/completion.ts +147 -24
- package/src/findings/dependencies-and-module-machinery.ts +90 -28
- package/src/findings/emitters.ts +1 -1
- package/src/findings/intra-function.ts +56 -14
- package/src/findings/non-code-artifacts.ts +81 -41
- package/src/findings/unused-declarations.ts +25 -1
- package/src/findings/visibility-narrowing.ts +32 -3
- package/src/generated-file.ts +16 -0
- package/src/git-ignore.ts +98 -0
- package/src/graph.ts +6 -0
- package/src/host.ts +5 -9
- package/src/import-bindings.ts +128 -0
- package/src/indexed-access.ts +152 -30
- package/src/interface-satisfaction.ts +33 -13
- package/src/intra-function-parts.ts +415 -64
- package/src/inventory.ts +83 -7
- package/src/json-document.ts +7 -7
- package/src/kinds.ts +13 -13
- package/src/matrix.ts +32 -14
- package/src/mdx-entries.ts +241 -0
- package/src/module-declarations.ts +11 -13
- package/src/namespace-objects.ts +34 -21
- package/src/overrides.ts +56 -9
- package/src/paths.ts +10 -0
- package/src/print.ts +1 -0
- package/src/ref.ts +12 -0
- package/src/references.ts +38 -17
- package/src/reflective-lookup.ts +9 -7
- package/src/report.ts +16 -3
- package/src/reporters/template.ts +21 -13
- package/src/resolve.ts +3 -0
- package/src/roots.ts +163 -45
- package/src/schema.ts +1 -0
- package/src/serialization-contract.ts +108 -17
- package/src/session.ts +29 -1
- package/src/source-files.ts +66 -16
- package/src/spelled-names.ts +102 -0
- package/src/suppress-file.ts +14 -3
- package/src/suppress-inline.ts +21 -2
- package/src/suppress.ts +9 -5
- package/src/sweep.ts +32 -11
- package/src/symbol-keys.ts +129 -2
- package/src/template-field.ts +9 -6
- package/src/test-support.ts +149 -16
- package/src/type-errors.ts +132 -41
- package/src/type-parameter-reads.ts +227 -0
- package/src/value-reach.ts +19 -6
- package/src/verbs/analyze.ts +28 -1
- package/src/verbs/explain.ts +5 -2
- package/src/version-range.ts +10 -5
- package/src/version.ts +1 -1
- package/src/workflow-steps.ts +127 -22
- package/src/workspace-programs.ts +7 -3
- package/src/workspace-resolution.ts +86 -14
- package/src/workspace.ts +1 -1
package/dist/src/entry-points.js
CHANGED
|
@@ -10,11 +10,12 @@
|
|
|
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, isCallExpression, isExportAssignment, isIdentifier, isMetaProperty, isNewExpression, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isStringLiteral, } from "@typescript/native/ast";
|
|
13
|
+
import { isArrayLiteralExpression, isAsExpression, isBinaryExpression, isCallExpression, isExportAssignment, isIdentifier, isMetaProperty, isNewExpression, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isStringLiteral, isTemplateExpression, SyntaxKind, } from "@typescript/native/ast";
|
|
14
14
|
import { SymbolFlags } from "@typescript/native/sync";
|
|
15
15
|
import { configurationFiles, filesNamedBy, moduleStrings, } from "./configuration-files.js";
|
|
16
16
|
import { globExpression } from "./glob.js";
|
|
17
17
|
import { htmlEntries } from "./html-entries.js";
|
|
18
|
+
import { documentEntries } from "./mdx-entries.js";
|
|
18
19
|
import { dirnamePath, relativePath, resolvePath } from "./paths.js";
|
|
19
20
|
import { UNANSWERED } from "./query.js";
|
|
20
21
|
import { workflowEntries } from "./workflow-steps.js";
|
|
@@ -37,8 +38,11 @@ const BROWSER_TEST_MATCH = "**/*.{spec,test}.{js,ts,jsx,tsx,cjs,cts,cjsx,ctsx,mj
|
|
|
37
38
|
const SETUP_MEMBERS = ["setupFiles", "globalSetup"];
|
|
38
39
|
/** A member that moves the directory every path of a test-runner configuration is read from. */
|
|
39
40
|
const ROOT_MEMBER = "root";
|
|
40
|
-
/**
|
|
41
|
-
|
|
41
|
+
/**
|
|
42
|
+
* A URL reference's scheme. A reference with none that does not start with a solidus is
|
|
43
|
+
* relative, the bare name included: https://url.spec.whatwg.org/#relative-url-string
|
|
44
|
+
*/
|
|
45
|
+
const URL_SCHEME = /^[a-z][a-z\d+.-]*:/iu;
|
|
42
46
|
/** The constructors whose first argument addresses a worker script. */
|
|
43
47
|
const WORKER_CONSTRUCTORS = new Set(["Worker", "SharedWorker"]);
|
|
44
48
|
/** The file's name, the last segment of its path. */
|
|
@@ -138,9 +142,16 @@ function literalsOf(value) {
|
|
|
138
142
|
}
|
|
139
143
|
return found;
|
|
140
144
|
}
|
|
141
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* Whether a literal is a relative URL reference, which is also how the test runner reads a
|
|
147
|
+
* setup file's path against its root.
|
|
148
|
+
*/
|
|
142
149
|
function isRelative(text) {
|
|
143
|
-
return
|
|
150
|
+
return text !== "" && !text.startsWith("/") && !URL_SCHEME.test(text);
|
|
151
|
+
}
|
|
152
|
+
/** Whether a module specifier is relative, which a bare name is not: it names a package. */
|
|
153
|
+
function isModuleRelative(text) {
|
|
154
|
+
return text.startsWith("./") || text.startsWith("../");
|
|
144
155
|
}
|
|
145
156
|
/** A URL reference with its query and its fragment removed, which name no part of the file. */
|
|
146
157
|
function pathPart(text) {
|
|
@@ -348,10 +359,10 @@ function workerSitesOf(file) {
|
|
|
348
359
|
return found;
|
|
349
360
|
}
|
|
350
361
|
/**
|
|
351
|
-
* The workers one file addresses. A call's names must resolve to the platform
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
362
|
+
* The workers one file addresses. A call's names must resolve to a symbol the platform
|
|
363
|
+
* declares, which a file of the target may augment and no import brings in; a name only
|
|
364
|
+
* the target declares is the target's own constructor or object, which addresses no
|
|
365
|
+
* worker this rule knows. One batch per file that writes such a call.
|
|
355
366
|
*/
|
|
356
367
|
function workersOf(project, file, files) {
|
|
357
368
|
const sites = workerSitesOf(file);
|
|
@@ -371,8 +382,7 @@ function workersOf(project, file, files) {
|
|
|
371
382
|
}
|
|
372
383
|
if (symbol !== undefined &&
|
|
373
384
|
(symbol.flags & SymbolFlags.Alias) === 0 &&
|
|
374
|
-
symbol.declarations.
|
|
375
|
-
symbol.declarations.every((handle) => !files.byName.has(handle.path))) {
|
|
385
|
+
symbol.declarations.some((handle) => !files.byName.has(handle.path))) {
|
|
376
386
|
platform.add(name);
|
|
377
387
|
}
|
|
378
388
|
});
|
|
@@ -388,6 +398,87 @@ function workersOf(project, file, files) {
|
|
|
388
398
|
}
|
|
389
399
|
return found;
|
|
390
400
|
}
|
|
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
|
+
/** Whether one call is `import.meta.glob(...)`. */
|
|
413
|
+
function isMetaGlob(node) {
|
|
414
|
+
if (!isCallExpression(node) || !isPropertyAccessExpression(node.expression)) {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
const { expression, name } = node.expression;
|
|
418
|
+
return (isIdentifier(name) &&
|
|
419
|
+
name.text === "glob" &&
|
|
420
|
+
isMetaProperty(expression) &&
|
|
421
|
+
expression.keywordToken === SyntaxKind.ImportKeyword);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* The files one file loads by a computed path, each with what exports: every own file
|
|
425
|
+
* below the directory a relative literal start of an `import()` or `require()` argument
|
|
426
|
+
* names, other than the calling file's own directory or one holding it, which would name
|
|
427
|
+
* every sibling, and
|
|
428
|
+
* every own file a relative literal glob of `import.meta.glob` matches, both read against
|
|
429
|
+
* the calling file's directory, as a bundler's context module reads them.
|
|
430
|
+
*/
|
|
431
|
+
function computedImportsOf(file, files) {
|
|
432
|
+
const dir = dirnamePath(file.fileName);
|
|
433
|
+
const found = [];
|
|
434
|
+
const below = (prefix) => {
|
|
435
|
+
const target = resolvePath(dir, prefix);
|
|
436
|
+
if (!isModuleRelative(prefix) ||
|
|
437
|
+
!prefix.endsWith("/") ||
|
|
438
|
+
target === dir ||
|
|
439
|
+
relativePath(target, dir) !== undefined) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
for (const candidate of files.byName.values()) {
|
|
443
|
+
if (candidate !== file && relativePath(target, candidate.fileName) !== undefined) {
|
|
444
|
+
found.push({ file: candidate, rule: "computed-import", source: prefix, exports: true });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
const globbed = (pattern) => {
|
|
449
|
+
if (!isModuleRelative(pattern)) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const expression = globExpression(resolvePath(dir, pattern));
|
|
453
|
+
for (const candidate of files.byName.values()) {
|
|
454
|
+
if (candidate !== file && expression.test(candidate.fileName)) {
|
|
455
|
+
found.push({ file: candidate, rule: "computed-import", source: pattern, exports: true });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
const visit = (node) => {
|
|
460
|
+
if (isCallExpression(node)) {
|
|
461
|
+
const [first] = node.arguments;
|
|
462
|
+
const callee = node.expression;
|
|
463
|
+
const loads = callee.kind === SyntaxKind.ImportKeyword ||
|
|
464
|
+
(isIdentifier(callee) && callee.text === "require");
|
|
465
|
+
if (loads && first !== undefined) {
|
|
466
|
+
const prefix = leadingLiteral(first);
|
|
467
|
+
if (prefix !== undefined) {
|
|
468
|
+
below(prefix);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
else if (isMetaGlob(node) && first !== undefined) {
|
|
472
|
+
for (const pattern of literalsOf(first) ?? []) {
|
|
473
|
+
globbed(pattern);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
node.forEachChild(visit);
|
|
478
|
+
};
|
|
479
|
+
file.forEachChild(visit);
|
|
480
|
+
return found;
|
|
481
|
+
}
|
|
391
482
|
/**
|
|
392
483
|
* Every file of one project the rules enter, in no particular order and possibly more
|
|
393
484
|
* than once: a file two rules enter appears once per rule.
|
|
@@ -417,10 +508,20 @@ export function entryPoints(project, files, testFiles, host, targetRoot, parse,
|
|
|
417
508
|
}
|
|
418
509
|
}
|
|
419
510
|
found.push(...workersOf(project, file, files));
|
|
511
|
+
found.push(...computedImportsOf(file, files));
|
|
420
512
|
}
|
|
421
513
|
for (const { file, source } of htmlEntries(host, targetRoot, files, parse)) {
|
|
422
514
|
found.push({ file, rule: "html-entry", source, exports: true });
|
|
423
515
|
}
|
|
516
|
+
for (const { file, source, names } of documentEntries(host, targetRoot, files)) {
|
|
517
|
+
found.push({
|
|
518
|
+
file,
|
|
519
|
+
rule: "document-import",
|
|
520
|
+
source,
|
|
521
|
+
exports: true,
|
|
522
|
+
...(names === undefined ? {} : { names }),
|
|
523
|
+
});
|
|
524
|
+
}
|
|
424
525
|
for (const { file, source } of workflowEntries(host, targetRoot, files)) {
|
|
425
526
|
found.push({ file, rule: "workflow-step", source, exports: true });
|
|
426
527
|
}
|
|
@@ -4,12 +4,27 @@
|
|
|
4
4
|
* together with every finding that falls with a root they withhold.
|
|
5
5
|
*/
|
|
6
6
|
import { componentId } from "../components.js";
|
|
7
|
+
import { deadComponents } from "../matrix.js";
|
|
7
8
|
import { OUTSIDE } from "../graph.js";
|
|
8
9
|
import { KINDS } from "../kinds.js";
|
|
9
10
|
import { byPosition, positionKey } from "../position.js";
|
|
10
11
|
import { declarationKey } from "../ref.js";
|
|
11
12
|
import { FAMILY_KEY_LENGTH } from "../resolve.js";
|
|
12
13
|
import { isPartSubject, isRowSubject } from "../suppress.js";
|
|
14
|
+
/** The unused-file code, whose deletion removes every use its file writes. */
|
|
15
|
+
const FILE_NEVER_IMPORTED = "DS1502";
|
|
16
|
+
/**
|
|
17
|
+
* The codes whose subject the run that counts test references holds live, as
|
|
18
|
+
* finding.schema.json lists them, so a test file's candidate reported under one carries
|
|
19
|
+
* none of a dead declaration's sweep facts.
|
|
20
|
+
*/
|
|
21
|
+
const LIVE_SUBJECT = new Set([
|
|
22
|
+
"DS1101",
|
|
23
|
+
"DS1102",
|
|
24
|
+
"DS1104",
|
|
25
|
+
"DS1204",
|
|
26
|
+
"DS1301",
|
|
27
|
+
]);
|
|
13
28
|
/** The fixability grammar/analysis.md "Fixability" gives a finding about a held declaration. */
|
|
14
29
|
const MANUAL = "manual";
|
|
15
30
|
/** The member kinds an object property can declare, which an `in` expression tests by name. */
|
|
@@ -72,15 +87,11 @@ function testedByIn(input) {
|
|
|
72
87
|
* published API, a public member of a published type included, is `certain` where the
|
|
73
88
|
* run loaded every consumer the scope declared and declared one, `probable` where one did
|
|
74
89
|
* not load, and `possible` where it declared none, whatever the configuration says of the
|
|
75
|
-
* consumer set.
|
|
76
|
-
* rule, what it contains, and a member an `in` expression tests by name are `possible`.
|
|
90
|
+
* consumer set. A member an `in` expression tests by name is `possible`.
|
|
77
91
|
*/
|
|
78
92
|
function reachabilityClasses(input) {
|
|
79
93
|
const tested = testedByIn(input);
|
|
80
94
|
const published = publishedIds(input.swept.matrix.union);
|
|
81
|
-
const usedByTests = new Set(input.swept.sweep.candidates
|
|
82
|
-
.filter((candidate) => candidate.heldByTests !== undefined)
|
|
83
|
-
.map((candidate) => candidate.id));
|
|
84
95
|
const { declared } = input.boundary.consumers;
|
|
85
96
|
const outside = declared.length === 0
|
|
86
97
|
? "possible"
|
|
@@ -88,7 +99,7 @@ function reachabilityClasses(input) {
|
|
|
88
99
|
? "certain"
|
|
89
100
|
: "probable";
|
|
90
101
|
return (id) => {
|
|
91
|
-
if (
|
|
102
|
+
if (tested.has(id)) {
|
|
92
103
|
return "possible";
|
|
93
104
|
}
|
|
94
105
|
return published.has(id) ? outside : "certain";
|
|
@@ -110,20 +121,33 @@ function testCeilings(input, classOf) {
|
|
|
110
121
|
for (const edge of union.out[union.at(id)] ?? []) {
|
|
111
122
|
const target = union.symbols[edge.to];
|
|
112
123
|
const testCode = union.test[edge.to] === true || union.support[edge.to] === true;
|
|
113
|
-
if (target !== undefined &&
|
|
124
|
+
if (target !== undefined &&
|
|
125
|
+
!testCode &&
|
|
126
|
+
union.ambient[edge.to] !== true &&
|
|
127
|
+
union.subject[edge.to] === true) {
|
|
114
128
|
lowest = capped(lowest, classOf(target.id));
|
|
115
129
|
}
|
|
116
130
|
}
|
|
117
131
|
return lowest;
|
|
118
132
|
};
|
|
119
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* 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
|
+
* the configuration declares the roots complete, because a root the analysis cannot see
|
|
138
|
+
* would make it live; every other declaration has none below `certain`.
|
|
139
|
+
*/
|
|
140
|
+
function rootsCeiling(input, finding, candidate) {
|
|
141
|
+
const unreachable = candidate?.relation === "reachability" || finding.unreachableOnly === true;
|
|
142
|
+
return unreachable && !input.config.ts.rootsComplete ? "probable" : "certain";
|
|
143
|
+
}
|
|
120
144
|
/**
|
|
121
145
|
* The component each dead declaration of the run falls in, and none for a live one, with
|
|
122
146
|
* its members listed in the canonical order where the run lists a component in full.
|
|
123
147
|
*/
|
|
124
|
-
function findingComponents(swept, cascade) {
|
|
148
|
+
function findingComponents(swept, components, cascade) {
|
|
125
149
|
const held = new Map();
|
|
126
|
-
for (const component of
|
|
150
|
+
for (const component of components) {
|
|
127
151
|
for (const member of component.members) {
|
|
128
152
|
held.set(member, component);
|
|
129
153
|
}
|
|
@@ -159,6 +183,59 @@ function findingComponents(swept, cascade) {
|
|
|
159
183
|
};
|
|
160
184
|
};
|
|
161
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* The run's dead components with each file a `DS1502` finding reports dead among them, so
|
|
188
|
+
* the file's declarations fall with it, and with the components each subject that is no
|
|
189
|
+
* declaration falls with joined into one.
|
|
190
|
+
*/
|
|
191
|
+
function runComponents(swept, findings) {
|
|
192
|
+
const union = swept.matrix.union;
|
|
193
|
+
const reported = new Set(findings.filter((one) => one.code === FILE_NEVER_IMPORTED).map((one) => one.position.path));
|
|
194
|
+
const files = new Map(union.symbols.flatMap((symbol, at) => symbol.kind === "file" && reported.has(symbol.position.path) ? [[at, symbol]] : []));
|
|
195
|
+
const computed = files.size === 0
|
|
196
|
+
? swept.sweep.components
|
|
197
|
+
: deadComponents(swept.matrix, swept.sweep, new Set(files.keys()));
|
|
198
|
+
const place = new Map();
|
|
199
|
+
computed.forEach((component, at) => {
|
|
200
|
+
for (const member of component.members) {
|
|
201
|
+
place.set(member, at);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
const leader = computed.map((_component, at) => at);
|
|
205
|
+
const find = (at) => {
|
|
206
|
+
let top = at;
|
|
207
|
+
while ((leader[top] ?? top) !== top) {
|
|
208
|
+
top = leader[top] ?? top;
|
|
209
|
+
}
|
|
210
|
+
return top;
|
|
211
|
+
};
|
|
212
|
+
for (const { fallsWith } of findings) {
|
|
213
|
+
const joined = (fallsWith ?? []).flatMap((id) => place.get(id) ?? []).map(find);
|
|
214
|
+
const first = Math.min(...joined);
|
|
215
|
+
for (const one of joined) {
|
|
216
|
+
leader[one] = first;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const merged = new Map();
|
|
220
|
+
computed.forEach((component, at) => {
|
|
221
|
+
const top = find(at);
|
|
222
|
+
const into = merged.get(top);
|
|
223
|
+
merged.set(top, into === undefined
|
|
224
|
+
? component
|
|
225
|
+
: {
|
|
226
|
+
id: into.id,
|
|
227
|
+
members: [...into.members, ...component.members],
|
|
228
|
+
roots: [...into.roots, ...component.roots],
|
|
229
|
+
deletableLines: into.deletableLines + component.deletableLines,
|
|
230
|
+
held: into.held && component.held,
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
return {
|
|
234
|
+
components: [...merged.values()],
|
|
235
|
+
computed: computed.length,
|
|
236
|
+
files: new Map([...files.values()].map((symbol) => [symbol.position.path, symbol.id])),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
162
239
|
/**
|
|
163
240
|
* Every finding of the run completed, in the order given. A dead declaration carries its
|
|
164
241
|
* candidate's relation, counts, configurations and component; a live one carries no relation and
|
|
@@ -174,10 +251,12 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
174
251
|
const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
|
|
175
252
|
const classOf = reachabilityClasses(input);
|
|
176
253
|
const ceilingOf = testCeilings(input, classOf);
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
254
|
+
const { components, computed, files } = runComponents(swept, findings);
|
|
255
|
+
const componentOf = findingComponents(swept, components, input.config.reporters.cascade);
|
|
256
|
+
const held = new Set(components.filter((one) => one.held).flatMap((one) => one.members));
|
|
257
|
+
// A join keeps its first component's identifier, so a minted one counts past every computed one.
|
|
258
|
+
let minted = computed;
|
|
259
|
+
const dead = new Set(components.map((component) => component.id));
|
|
181
260
|
const fallingWith = new Map();
|
|
182
261
|
for (const { heldByTests } of swept.sweep.candidates) {
|
|
183
262
|
if (heldByTests !== undefined) {
|
|
@@ -193,18 +272,30 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
193
272
|
// a row is still never the declaration. Two declarations of one file can share a
|
|
194
273
|
// reference, so the one the finding is positioned at is the subject where there is one.
|
|
195
274
|
const declared = union.at(positionKey(finding.position));
|
|
275
|
+
const ref = finding.symbol.holder ?? finding.symbol.ref;
|
|
196
276
|
const at = isRowSubject(finding.symbol.kind)
|
|
197
277
|
? OUTSIDE
|
|
198
|
-
: union.symbols[declared]?.ref ===
|
|
278
|
+
: union.symbols[declared]?.ref === ref
|
|
199
279
|
? declared
|
|
200
|
-
: (positions.get(declarationKey(
|
|
280
|
+
: (positions.get(declarationKey(ref, finding.position.path)) ?? OUTSIDE);
|
|
201
281
|
// A part names the declaration that holds it, and is decided inside it rather than by
|
|
202
282
|
// the declaration's liveness, so it carries none of the declaration's sweep facts.
|
|
203
283
|
const part = isPartSubject(finding.symbol.kind);
|
|
204
284
|
const symbol = union.symbols[at];
|
|
205
285
|
const judged = !part && union.subject[at] === true ? symbol : undefined;
|
|
206
|
-
const candidate = judged === undefined
|
|
207
|
-
|
|
286
|
+
const candidate = judged === undefined || LIVE_SUBJECT.has(finding.code)
|
|
287
|
+
? undefined
|
|
288
|
+
: candidates.get(judged.id);
|
|
289
|
+
const fallsWith = finding.fallsWith?.[0];
|
|
290
|
+
const joined = fallsWith === undefined ? undefined : componentOf(fallsWith);
|
|
291
|
+
const file = finding.code === FILE_NEVER_IMPORTED ? files.get(finding.position.path) : undefined;
|
|
292
|
+
const found = joined !== undefined
|
|
293
|
+
? { ...joined, root: false }
|
|
294
|
+
: file !== undefined
|
|
295
|
+
? componentOf(file)
|
|
296
|
+
: symbol === undefined || part
|
|
297
|
+
? undefined
|
|
298
|
+
: componentOf(symbol.id);
|
|
208
299
|
// A test file's declaration is a component member only as a test of dead code, and
|
|
209
300
|
// test code keeps the test-support code it references out of every component.
|
|
210
301
|
if (candidate !== undefined &&
|
|
@@ -236,9 +327,13 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
236
327
|
row.findingFixabilities?.includes(MANUAL) === true
|
|
237
328
|
? MANUAL
|
|
238
329
|
: row.fixability;
|
|
239
|
-
const lastUses = fixability
|
|
240
|
-
?
|
|
241
|
-
: undefined
|
|
330
|
+
const lastUses = fixability !== "deletable"
|
|
331
|
+
? undefined
|
|
332
|
+
: candidate !== undefined
|
|
333
|
+
? input.dependencies.lastUses.get(candidate.id)
|
|
334
|
+
: finding.code === FILE_NEVER_IMPORTED
|
|
335
|
+
? input.dependencies.fileLastUses.get(finding.position.path)
|
|
336
|
+
: undefined;
|
|
242
337
|
return {
|
|
243
338
|
code: finding.code,
|
|
244
339
|
kind: row.name,
|
|
@@ -246,7 +341,7 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
246
341
|
position: finding.position,
|
|
247
342
|
symbol: finding.symbol,
|
|
248
343
|
reachabilityClass,
|
|
249
|
-
confidence: capped(capped(reachabilityClass, row.maxClass), judged === undefined ? "certain" : ceilingOf(judged.id)),
|
|
344
|
+
confidence: capped(capped(capped(reachabilityClass, row.maxClass), rootsCeiling(input, finding, candidate)), judged === undefined ? "certain" : ceilingOf(judged.id)),
|
|
250
345
|
...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
|
|
251
346
|
testOnly: judged !== undefined && counts?.production === 0 && counts.test > 0,
|
|
252
347
|
generated,
|
|
@@ -1,38 +1,84 @@
|
|
|
1
1
|
import { declarationKey } from "../dependencies.js";
|
|
2
2
|
import { renderRef } from "../ref.js";
|
|
3
|
+
import { nonCodeArtifacts } from "./non-code-artifacts.js";
|
|
3
4
|
const UNUSED_DEPENDENCY = "DS1601";
|
|
5
|
+
const FILE_NEVER_IMPORTED = "DS1502";
|
|
4
6
|
const MESSAGES = {
|
|
5
7
|
dependency: "no import in the target needs the declared dependency",
|
|
6
8
|
"dev-dependency": "no import in the target needs the declared development dependency",
|
|
7
9
|
"peer-dependency": "no import in the target needs the declared peer dependency",
|
|
8
10
|
};
|
|
11
|
+
const DEAD_USERS_MESSAGES = {
|
|
12
|
+
dependency: "only code that is itself dead uses the declared dependency",
|
|
13
|
+
"dev-dependency": "only code that is itself dead uses the declared development dependency",
|
|
14
|
+
"peer-dependency": "only code that is itself dead uses the declared peer dependency",
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* The run's dead code: test code keeps the code it references out of every dead component
|
|
18
|
+
* unless that code is dead as well, and a file's top level is dead where the file is.
|
|
19
|
+
*/
|
|
20
|
+
function deadCode(input) {
|
|
21
|
+
const { swept } = input;
|
|
22
|
+
const union = swept.matrix.union;
|
|
23
|
+
const files = new Map(union.symbols.filter((one) => one.kind === "file").map((one) => [one.position.path, one.id]));
|
|
24
|
+
const dead = new Set(swept.sweep.components.flatMap((component) => component.members));
|
|
25
|
+
const reachedOnly = new Set(swept.sweep.candidates.filter((one) => one.relation === "reachability").map((one) => one.id));
|
|
26
|
+
for (const finding of nonCodeArtifacts(input)) {
|
|
27
|
+
const id = files.get(finding.position.path);
|
|
28
|
+
if (finding.code === FILE_NEVER_IMPORTED && id !== undefined) {
|
|
29
|
+
dead.add(id);
|
|
30
|
+
if (finding.unreachableOnly === true) {
|
|
31
|
+
reachedOnly.add(id);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const published = new Set(union.rooted
|
|
36
|
+
.filter((root) => root.kind === "published-api")
|
|
37
|
+
.flatMap((root) => union.symbols[root.at]?.id ?? []));
|
|
38
|
+
return { dead, reachedOnly, published };
|
|
39
|
+
}
|
|
9
40
|
/**
|
|
10
41
|
* A dependency, development dependency or peer dependency of a manifest the run reads
|
|
11
|
-
* that the target does not need, at the position of its key
|
|
42
|
+
* that the target does not need, at the position of its key, and one whose every use is
|
|
43
|
+
* held by dead code, which is dead only through that code and falls with its component:
|
|
44
|
+
* at `possible` where one of its holders is published, and found under reachability alone
|
|
45
|
+
* where one of them is.
|
|
12
46
|
*/
|
|
13
|
-
function unusedDependencies(dependencies) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
47
|
+
function unusedDependencies(input, dependencies) {
|
|
48
|
+
const { dead, reachedOnly, published } = deadCode(input);
|
|
49
|
+
return dependencies.declared.flatMap((dependency) => {
|
|
50
|
+
const key = declarationKey(dependency);
|
|
51
|
+
const held = dependencies.heldBy.get(key);
|
|
52
|
+
const users = held?.every((id) => dead.has(id)) === true ? held : undefined;
|
|
53
|
+
if (dependencies.ran.has(key) || (dependencies.needed.has(key) && users === undefined)) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
const unbuilt = dependencies.unbuilt.has(dependency.position.path);
|
|
57
|
+
const atPossible = unbuilt || users?.some((id) => published.has(id)) === true;
|
|
58
|
+
return [
|
|
59
|
+
{
|
|
60
|
+
code: UNUSED_DEPENDENCY,
|
|
61
|
+
position: dependency.position,
|
|
62
|
+
symbol: {
|
|
63
|
+
ref: renderRef(dependency.manifest, {
|
|
64
|
+
of: "dependency",
|
|
65
|
+
name: dependency.name,
|
|
66
|
+
section: dependency.section,
|
|
67
|
+
}),
|
|
68
|
+
kind: "dependency",
|
|
69
|
+
name: dependency.name,
|
|
70
|
+
sizeLines: 1,
|
|
71
|
+
},
|
|
72
|
+
message: (users === undefined ? MESSAGES : DEAD_USERS_MESSAGES)[dependency.section],
|
|
73
|
+
details: { dependencyClass: dependency.section },
|
|
74
|
+
...(atPossible ? { reachabilityClass: "possible" } : {}),
|
|
75
|
+
...(users?.some((id) => reachedOnly.has(id)) === true
|
|
76
|
+
? { unreachableOnly: true }
|
|
77
|
+
: {}),
|
|
78
|
+
...(users === undefined ? {} : { fallsWith: users }),
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
});
|
|
36
82
|
}
|
|
37
83
|
/** The findings of the dependencies-and-module-machinery family, `DS1600` to `DS1699`. */
|
|
38
|
-
export const dependenciesAndModuleMachinery = (
|
|
84
|
+
export const dependenciesAndModuleMachinery = (input) => unusedDependencies(input, input.dependencies);
|
|
@@ -100,7 +100,7 @@ function dialledOver(input, suppressed, found, unmarked) {
|
|
|
100
100
|
const unanswered = heldKeys(input);
|
|
101
101
|
const markup = markupKeys(input);
|
|
102
102
|
const candidates = found.filter((finding) => {
|
|
103
|
-
const key = declarationKey(finding.symbol.ref, finding.position.path);
|
|
103
|
+
const key = declarationKey(finding.symbol.holder ?? finding.symbol.ref, finding.position.path);
|
|
104
104
|
return (!ledger.withheld(finding) &&
|
|
105
105
|
!unanswered.has(key) &&
|
|
106
106
|
!markup.has(key) &&
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { KINDS } from "../kinds.js";
|
|
2
|
+
import { parameterRef } from "../ref.js";
|
|
2
3
|
import { UNUSED_PARAMETER, UNUSED_RESULT } from "../intra-function-parts.js";
|
|
3
4
|
import { reachedWithTheTests } from "../sweep.js";
|
|
4
5
|
/** A list of rules as a sentence names them. */
|
|
@@ -8,24 +9,43 @@ function spelled(rules) {
|
|
|
8
9
|
}
|
|
9
10
|
return `${rules.slice(0, -1).join(", ")} and ${rules.at(-1) ?? ""}`;
|
|
10
11
|
}
|
|
11
|
-
/** The
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
/** The compiler's rule of the parameter kind's overlap list, which reports only in a file it checks. */
|
|
13
|
+
const COMPILER_PARAMETER_RULE = "tsc --noUnusedParameters";
|
|
14
|
+
/**
|
|
15
|
+
* The clause a message ends with: the other rules that report the part, or that none is
|
|
16
|
+
* known. A parameter of a file the compiler does not check is no report of the compiler's.
|
|
17
|
+
*/
|
|
18
|
+
function overlapClause(overlap, part) {
|
|
19
|
+
const rules = part.parameter?.checked === false
|
|
20
|
+
? overlap.filter((rule) => rule !== COMPILER_PARAMETER_RULE)
|
|
21
|
+
: overlap;
|
|
22
|
+
return rules.length === 1 && rules[0] === "none known"
|
|
14
23
|
? "no other rule is known to report it"
|
|
15
|
-
: `also reported by ${spelled(
|
|
24
|
+
: `also reported by ${spelled(rules)}`;
|
|
16
25
|
}
|
|
17
26
|
/**
|
|
18
|
-
* Whether one parameter
|
|
19
|
-
*
|
|
20
|
-
*
|
|
27
|
+
* Whether one unread parameter is reported, and how. Of a free signature it is deleted. A
|
|
28
|
+
* caller may pass a function used as a value more arguments than it declares, so there one
|
|
29
|
+
* a read parameter follows is renamed to an underscore name, which the compilers read as
|
|
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.
|
|
21
32
|
*/
|
|
22
|
-
function
|
|
33
|
+
function parameterVerdict(part, free, freeButValued) {
|
|
23
34
|
const place = part.parameter;
|
|
24
35
|
if (place?.inValue !== true && free.has(part.declaration)) {
|
|
25
|
-
return
|
|
36
|
+
return place?.annotated === undefined ? { fix: "delete", confidence: "certain" } : undefined;
|
|
26
37
|
}
|
|
27
38
|
const valued = place?.inValue === true || freeButValued.has(part.declaration);
|
|
28
|
-
|
|
39
|
+
if (!valued) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
if (place?.beforeARead === true) {
|
|
43
|
+
return { fix: "rename" };
|
|
44
|
+
}
|
|
45
|
+
if (place?.annotated === "steers") {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
return { fix: "delete", confidence: place?.annotated === "free" ? "probable" : "certain" };
|
|
29
49
|
}
|
|
30
50
|
/**
|
|
31
51
|
* Whether one part is reported. A parameter needs its function's signature free to
|
|
@@ -41,7 +61,7 @@ function reported(input, part, callersUnknown) {
|
|
|
41
61
|
const { free, freeButValued, discardedEverywhere } = input.intraFunction;
|
|
42
62
|
switch (part.code) {
|
|
43
63
|
case UNUSED_PARAMETER:
|
|
44
|
-
return
|
|
64
|
+
return parameterVerdict(part, free, freeButValued) !== undefined;
|
|
45
65
|
case UNUSED_RESULT:
|
|
46
66
|
return (free.has(part.declaration) &&
|
|
47
67
|
discardedEverywhere.has(part.declaration) &&
|
|
@@ -96,20 +116,31 @@ export const intraFunction = (input) => {
|
|
|
96
116
|
if (declaration === undefined || !judged(declaration.id) || !reported(input, part, unknown)) {
|
|
97
117
|
continue;
|
|
98
118
|
}
|
|
119
|
+
const verdict = part.code === UNUSED_PARAMETER
|
|
120
|
+
? parameterVerdict(part, input.intraFunction.free, input.intraFunction.freeButValued)
|
|
121
|
+
: undefined;
|
|
122
|
+
const message = verdict?.fix === "rename"
|
|
123
|
+
? `${part.message}, and a read parameter follows it, so rename it to _${part.name}`
|
|
124
|
+
: part.message;
|
|
99
125
|
found.push({
|
|
100
126
|
code: part.code,
|
|
101
127
|
position: part.position,
|
|
102
128
|
symbol: {
|
|
103
|
-
|
|
129
|
+
...(part.code === UNUSED_PARAMETER
|
|
130
|
+
? { ref: parameterRef(declaration.ref, part.name), holder: declaration.ref }
|
|
131
|
+
: { ref: declaration.ref }),
|
|
104
132
|
kind: part.kind,
|
|
105
133
|
name: part.name,
|
|
106
134
|
sizeLines: part.position.endLine - part.position.line + 1,
|
|
107
135
|
},
|
|
108
|
-
message: `${
|
|
136
|
+
message: `${message}; ${overlapClause(overlap, part)}`,
|
|
109
137
|
details: {
|
|
110
138
|
...(part.kind === "store" ? { writePositions: [part.position] } : {}),
|
|
111
139
|
overlap,
|
|
112
140
|
},
|
|
141
|
+
...(verdict?.fix === "delete" && verdict.confidence !== "certain"
|
|
142
|
+
? { reachabilityClass: verdict.confidence }
|
|
143
|
+
: {}),
|
|
113
144
|
});
|
|
114
145
|
}
|
|
115
146
|
return found;
|