@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
|
@@ -2,6 +2,7 @@ import type { Finding } from "../finding.ts";
|
|
|
2
2
|
import type { InventorySymbol, SymbolKind } from "../inventory.ts";
|
|
3
3
|
import type { MatrixCandidate } from "../matrix.ts";
|
|
4
4
|
import type { Emitter, EmitterInput } from "./emitter.ts";
|
|
5
|
+
import { testsUnseenAPI } from "./completion.ts";
|
|
5
6
|
import { unreachableExports } from "./visibility-narrowing.ts";
|
|
6
7
|
|
|
7
8
|
/** The kinds of declaration that are members of a class, an interface, a type or an enum. */
|
|
@@ -55,19 +56,22 @@ interface Judged {
|
|
|
55
56
|
readonly deprecated: boolean;
|
|
56
57
|
/** Whether the unreachable-export kind reports it. */
|
|
57
58
|
readonly unreachable: boolean;
|
|
59
|
+
/** Whether it is a test with a target that is published API no declared consumer judges. */
|
|
60
|
+
readonly testsUnseenAPI: boolean;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
/**
|
|
61
64
|
* The code one candidate is reported under, or none, each symbol once under the most
|
|
62
65
|
* specific code: a test of dead code first; no other test-file declaration or enum
|
|
63
66
|
* member nothing names; a test-support declaration test code references as test-only
|
|
64
|
-
* use; then deprecation, a test reference,
|
|
65
|
-
* unused export being the
|
|
67
|
+
* use; then deprecation, a test reference, or only dead production references beside a
|
|
68
|
+
* test that stays, and the unreferenced codes, an unimportable unused export being the
|
|
69
|
+
* unreachable kind's.
|
|
66
70
|
*/
|
|
67
71
|
function codeOf(judged: Judged): string | undefined {
|
|
68
72
|
const { candidate, symbol } = judged;
|
|
69
73
|
if (candidate.testOfDeadCode) {
|
|
70
|
-
return "DS1005";
|
|
74
|
+
return judged.testsUnseenAPI ? undefined : "DS1005";
|
|
71
75
|
}
|
|
72
76
|
if ((judged.test && !candidate.unreferencedTest) || NOT_THIS_FAMILY.has(symbol.kind)) {
|
|
73
77
|
return undefined;
|
|
@@ -85,7 +89,11 @@ function codeOf(judged: Judged): string | undefined {
|
|
|
85
89
|
if (candidate.productionRefs === 0 && judged.deprecated) {
|
|
86
90
|
return "DS1006";
|
|
87
91
|
}
|
|
88
|
-
if (
|
|
92
|
+
if (
|
|
93
|
+
candidate.testRefs > 0 &&
|
|
94
|
+
(candidate.productionRefs === 0 ||
|
|
95
|
+
(candidate.relation === "reachability" && candidate.keptByTests))
|
|
96
|
+
) {
|
|
89
97
|
return "DS1004";
|
|
90
98
|
}
|
|
91
99
|
if (MEMBERS.has(symbol.kind)) {
|
|
@@ -132,7 +140,9 @@ function messageOf(code: string, judged: Judged): string {
|
|
|
132
140
|
case "DS1006":
|
|
133
141
|
return `deprecated ${word} has no production reference`;
|
|
134
142
|
case "DS1004":
|
|
135
|
-
return
|
|
143
|
+
return candidate.productionRefs === 0
|
|
144
|
+
? `${word} is referenced only from test files and never from production code`
|
|
145
|
+
: `${word} is referenced only from test files and from declarations that are themselves dead`;
|
|
136
146
|
}
|
|
137
147
|
const subject =
|
|
138
148
|
code === "DS1003"
|
|
@@ -234,6 +244,7 @@ function familyFindings(input: EmitterInput, fallen: boolean): Finding[] {
|
|
|
234
244
|
const union = input.swept.matrix.union;
|
|
235
245
|
const dead = new Set(input.swept.sweep.candidates.map((candidate) => candidate.id));
|
|
236
246
|
const unreachable = unreachableExports(input);
|
|
247
|
+
const unseenAPI = testsUnseenAPI(input);
|
|
237
248
|
let brands: ReadonlySet<string> | undefined;
|
|
238
249
|
|
|
239
250
|
const found: Finding[] = [];
|
|
@@ -253,6 +264,7 @@ function familyFindings(input: EmitterInput, fallen: boolean): Finding[] {
|
|
|
253
264
|
test: union.test[at] === true,
|
|
254
265
|
deprecated: input.deprecated.has(symbol.id),
|
|
255
266
|
unreachable: unreachable.has(symbol.id),
|
|
267
|
+
testsUnseenAPI: candidate.testOfDeadCode && unseenAPI(candidate.id),
|
|
256
268
|
};
|
|
257
269
|
const code = codeOf(judged);
|
|
258
270
|
if (
|
|
@@ -63,6 +63,12 @@ function entersByName(kinds: ReadonlySet<RootKind> | undefined): boolean {
|
|
|
63
63
|
return [...(kinds ?? [])].some((kind) => !PUBLISHING.has(kind) && !RUN_ONLY.has(kind));
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* The root kinds whose root of a file enters no export by name: a document's import roots
|
|
68
|
+
* each export it imports, so an export it does not import is judged as in any file.
|
|
69
|
+
*/
|
|
70
|
+
const PER_NAME: ReadonlySet<RootKind> = new Set<RootKind>(["document-import"]);
|
|
71
|
+
|
|
66
72
|
/**
|
|
67
73
|
* Whether the roots of a declaration's file enter it by name: a test runner's root
|
|
68
74
|
* enters only an export the runner calls, so another export of a test file is judged.
|
|
@@ -72,7 +78,7 @@ function fileEntersByName(
|
|
|
72
78
|
symbol: InventorySymbol,
|
|
73
79
|
): boolean {
|
|
74
80
|
const entering = [...(kinds ?? [])].filter(
|
|
75
|
-
(kind) => !PUBLISHING.has(kind) && !RUN_ONLY.has(kind),
|
|
81
|
+
(kind) => !PUBLISHING.has(kind) && !RUN_ONLY.has(kind) && !PER_NAME.has(kind),
|
|
76
82
|
);
|
|
77
83
|
return (
|
|
78
84
|
entering.some((kind) => kind !== "test-runner") || (entering.length > 0 && runnerCalls(symbol))
|
|
@@ -222,7 +228,7 @@ function narrowing(
|
|
|
222
228
|
* export no code outside the target can import, because the manifest declares
|
|
223
229
|
* `exports` and no root reaches it. Each arm yields to a more specific kind, so one
|
|
224
230
|
* declaration is reported once: a test whose every subject is dead, an interface, a
|
|
225
|
-
* declaration only a test file references, and a deprecated one.
|
|
231
|
+
* declaration only a test file references or a test that stays keeps, and a deprecated one.
|
|
226
232
|
*/
|
|
227
233
|
function unreachable(
|
|
228
234
|
facts: Facts,
|
|
@@ -237,7 +243,7 @@ function unreachable(
|
|
|
237
243
|
(facts.rootedBy[at]?.size ?? 0) > 0 ||
|
|
238
244
|
candidate.testOfDeadCode ||
|
|
239
245
|
symbol.kind === "interface" ||
|
|
240
|
-
(candidate.
|
|
246
|
+
(candidate.testRefs > 0 && (candidate.productionRefs === 0 || candidate.keptByTests)) ||
|
|
241
247
|
(candidate.productionRefs === 0 && input.deprecated.has(symbol.id))
|
|
242
248
|
) {
|
|
243
249
|
return undefined;
|
package/src/graph.ts
CHANGED
|
@@ -41,9 +41,9 @@ const SWEPT: Readonly<Record<SymbolKind, boolean>> = {
|
|
|
41
41
|
* Whether a root of each kind names a caller, which holds it live under both relations: a
|
|
42
42
|
* tool reads what it loads and what its strings name, a runtime runs a test, setup or
|
|
43
43
|
* worker file, a page's module script, a command, a script or a workflow step, the manifest
|
|
44
|
-
* names what enters the package, an alias global stands for its export,
|
|
45
|
-
*
|
|
46
|
-
*
|
|
44
|
+
* names what enters the package, an alias global stands for its export, an outside
|
|
45
|
+
* declaration names a global interface merging into it, and an entry file, a configured root
|
|
46
|
+
* and a pattern assert a caller. A published API supposes a consumer: reachability alone.
|
|
47
47
|
*/
|
|
48
48
|
const NAMES_A_CALLER: Readonly<Record<RootKind, boolean>> = {
|
|
49
49
|
"entry-file": true,
|
|
@@ -56,8 +56,10 @@ const NAMES_A_CALLER: Readonly<Record<RootKind, boolean>> = {
|
|
|
56
56
|
pattern: true,
|
|
57
57
|
"type-error": true,
|
|
58
58
|
"type-query-alias": true,
|
|
59
|
+
"global-augmentation": true,
|
|
59
60
|
"configuration-file": true,
|
|
60
61
|
"configuration-string": true,
|
|
62
|
+
"bundler-entry": true,
|
|
61
63
|
"test-runner": true,
|
|
62
64
|
"browser-tests": true,
|
|
63
65
|
worker: true,
|
|
@@ -137,6 +137,12 @@ interface ParameterPlace {
|
|
|
137
137
|
* so the annotation may steer the call's inference.
|
|
138
138
|
*/
|
|
139
139
|
readonly annotated?: "steers" | "free";
|
|
140
|
+
/**
|
|
141
|
+
* Whether the name is one an object binding pattern of a parameter binds, and the
|
|
142
|
+
* compiler gives its function a contextual type: the code handing the argument may act on
|
|
143
|
+
* the pattern's read of the property, which the program does not show.
|
|
144
|
+
*/
|
|
145
|
+
readonly contextualPattern?: true;
|
|
140
146
|
/** Whether the compiler checks the parameter's file, so its unused-parameter rule may report it. */
|
|
141
147
|
readonly checked: boolean;
|
|
142
148
|
}
|
|
@@ -1093,10 +1099,9 @@ function calleeSteers<Brand>(
|
|
|
1093
1099
|
|
|
1094
1100
|
/**
|
|
1095
1101
|
* The parts with each name of `contextual` judged where the compiler gives its function a
|
|
1096
|
-
* contextual type:
|
|
1097
|
-
*
|
|
1098
|
-
*
|
|
1099
|
-
* name read.
|
|
1102
|
+
* contextual type: a name an object pattern binds is marked as one, and an annotated
|
|
1103
|
+
* parameter with whether the call's inference may read its annotation. A question the
|
|
1104
|
+
* checker leaves unanswered keeps the name read.
|
|
1100
1105
|
*/
|
|
1101
1106
|
function withContextualRead<Brand>(
|
|
1102
1107
|
project: ProjectView<Brand>,
|
|
@@ -1118,15 +1123,17 @@ function withContextualRead<Brand>(
|
|
|
1118
1123
|
const place = one.part.parameter;
|
|
1119
1124
|
judged.set(
|
|
1120
1125
|
one.part,
|
|
1121
|
-
|
|
1126
|
+
answer === UNANSWERED || place === undefined
|
|
1122
1127
|
? { ...one.part, used: true }
|
|
1123
|
-
:
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
...
|
|
1127
|
-
|
|
1128
|
+
: one.pattern
|
|
1129
|
+
? { ...one.part, parameter: { ...place, contextualPattern: true } }
|
|
1130
|
+
: {
|
|
1131
|
+
...one.part,
|
|
1132
|
+
parameter: {
|
|
1133
|
+
...place,
|
|
1134
|
+
annotated: calleeSteers(project, one.signature, one.at) ? "steers" : "free",
|
|
1135
|
+
},
|
|
1128
1136
|
},
|
|
1129
|
-
},
|
|
1130
1137
|
);
|
|
1131
1138
|
}
|
|
1132
1139
|
return parts.map((part) => judged.get(part) ?? part);
|
|
@@ -1205,6 +1212,8 @@ interface Local {
|
|
|
1205
1212
|
readonly uses: readonly Identifier[];
|
|
1206
1213
|
/** A `var` of a nested block, whose function scope outlives the block's end. */
|
|
1207
1214
|
readonly outlives: boolean;
|
|
1215
|
+
/** A function declaration, written at the start of the block whatever its place. */
|
|
1216
|
+
readonly hoisted: boolean;
|
|
1208
1217
|
}
|
|
1209
1218
|
|
|
1210
1219
|
/**
|
|
@@ -1244,9 +1253,10 @@ function functionOf(block: Block): FunctionLikeDeclaration | undefined {
|
|
|
1244
1253
|
}
|
|
1245
1254
|
|
|
1246
1255
|
/**
|
|
1247
|
-
* The locals one block declares with `let`, `const
|
|
1248
|
-
* block that spell it. A `var` of a nested
|
|
1249
|
-
*
|
|
1256
|
+
* The locals one block declares with `let`, `const`, `var` or a function declaration
|
|
1257
|
+
* inside a function, each with the names of the block that spell it. A `var` of a nested
|
|
1258
|
+
* block is one where no nested function of its function names it, since the function scope
|
|
1259
|
+
* holds it past the block's end.
|
|
1250
1260
|
*/
|
|
1251
1261
|
function localsOf(block: Block): readonly Local[] {
|
|
1252
1262
|
const declared: {
|
|
@@ -1254,10 +1264,23 @@ function localsOf(block: Block): readonly Local[] {
|
|
|
1254
1264
|
statement: number;
|
|
1255
1265
|
initialized: boolean;
|
|
1256
1266
|
outlives: boolean;
|
|
1267
|
+
hoisted: boolean;
|
|
1257
1268
|
}[] = [];
|
|
1258
1269
|
const scope = functionOf(block);
|
|
1259
1270
|
const outlives = scope?.body !== block;
|
|
1260
1271
|
block.statements.forEach((statement, at) => {
|
|
1272
|
+
if (isFunctionDeclaration(statement)) {
|
|
1273
|
+
if (scope !== undefined && statement.name !== undefined && statement.body !== undefined) {
|
|
1274
|
+
declared.push({
|
|
1275
|
+
name: statement.name,
|
|
1276
|
+
statement: at,
|
|
1277
|
+
initialized: true,
|
|
1278
|
+
outlives: false,
|
|
1279
|
+
hoisted: true,
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1261
1284
|
if (!isVariableStatement(statement)) {
|
|
1262
1285
|
return;
|
|
1263
1286
|
}
|
|
@@ -1267,6 +1290,11 @@ function localsOf(block: Block): readonly Local[] {
|
|
|
1267
1290
|
return;
|
|
1268
1291
|
}
|
|
1269
1292
|
for (const declaration of list.declarations) {
|
|
1293
|
+
// A write to a local typed `never` is the compiler's exhaustiveness check, which
|
|
1294
|
+
// fails to compile once a case goes unhandled.
|
|
1295
|
+
if (declaration.type?.kind === SyntaxKind.NeverKeyword) {
|
|
1296
|
+
continue;
|
|
1297
|
+
}
|
|
1270
1298
|
for (const name of boundNames(declaration.name)) {
|
|
1271
1299
|
if (
|
|
1272
1300
|
!blockScoped &&
|
|
@@ -1282,6 +1310,7 @@ function localsOf(block: Block): readonly Local[] {
|
|
|
1282
1310
|
statement: at,
|
|
1283
1311
|
initialized: declaration.initializer !== undefined,
|
|
1284
1312
|
outlives: !blockScoped && outlives,
|
|
1313
|
+
hoisted: false,
|
|
1285
1314
|
});
|
|
1286
1315
|
}
|
|
1287
1316
|
}
|
|
@@ -1333,12 +1362,22 @@ function deadStores(
|
|
|
1333
1362
|
used: false,
|
|
1334
1363
|
});
|
|
1335
1364
|
for (const local of locals) {
|
|
1336
|
-
const
|
|
1365
|
+
const own = local.hoisted ? block.statements[local.statement] : undefined;
|
|
1366
|
+
// A function's reads of itself keep it no more than its absence would.
|
|
1367
|
+
const uses = local.uses.filter(
|
|
1368
|
+
(use) => readsBinding(use, local.name, ids) && (own === undefined || !within(use, own)),
|
|
1369
|
+
);
|
|
1337
1370
|
if (!local.outlives && uses.every(isPlainWrite)) {
|
|
1338
|
-
|
|
1371
|
+
// A declaration with no initializer writes `undefined`, the one store left to name.
|
|
1372
|
+
const writes = [...(local.initialized ? [local.name] : []), ...uses];
|
|
1373
|
+
const targets = writes.length === 0 ? [local.name] : writes;
|
|
1339
1374
|
found.push(...targets.map((target) => deadAt(local, target)));
|
|
1340
1375
|
continue;
|
|
1341
1376
|
}
|
|
1377
|
+
// A read anywhere in the block reads a function declaration, which is written first.
|
|
1378
|
+
if (local.hoisted) {
|
|
1379
|
+
continue;
|
|
1380
|
+
}
|
|
1342
1381
|
if (uses.some((use) => inNestedFunction(use, block))) {
|
|
1343
1382
|
continue;
|
|
1344
1383
|
}
|
package/src/kinds.ts
CHANGED
|
@@ -76,7 +76,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
76
76
|
code: "DS1004",
|
|
77
77
|
name: "test-only-use",
|
|
78
78
|
languages: ["go", "ts"],
|
|
79
|
-
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 tests reach and that carries test evidence of its own language, is a test reference. A declaration of test-support code is test code and is no subject of this code, as grammar/analysis.md states. An interface method whose only calls and selections through the interface are in test code, and that DS1203's precondition does not exempt, is reported under this code. The methods implementing it are not reported with it, because the test reaches them through it.",
|
|
79
|
+
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 tests reach and that carries test evidence of its own language, is a test reference. A declaration of test-support code is test code and is no subject of this code, as grammar/analysis.md states. An interface method whose only calls and selections through the interface are in test code, and that DS1203's precondition does not exempt, is reported under this code. The methods implementing it are not reported with it, because the test reaches them through it. A candidate found dead under reachability, every production reference to which is made by a declaration itself dead, is reported under this code as well where test code that is no test of dead code, and that the run counting test references reaches, references it, because that test keeps it, and the finding's message names both kinds of reference.",
|
|
80
80
|
defaultSeverity: "deny",
|
|
81
81
|
maxClass: "certain",
|
|
82
82
|
fixability: "deletable",
|
|
@@ -87,7 +87,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
87
87
|
code: "DS1005",
|
|
88
88
|
name: "test-of-dead-code",
|
|
89
89
|
languages: ["go", "ts"],
|
|
90
|
-
rule: "A test symbol whose set of referenced target symbols is non-empty and every member of that set is reported dead. In TypeScript an ambient declaration, one a declaration file holds or one a `declare` modifier carries or a declaration carrying one holds, describes code the target does not implement, so it is no target symbol of that set. A test that references at least one live target symbol is never reported, no notion of a test's subject and no name matching enters the rule, and the message states the rule. The test joins the dead component of the symbols it references, and it and each of them count as referencing each other, so the test is a member of their cycle and never a dead symbol outside it.",
|
|
90
|
+
rule: "A test symbol whose set of referenced target symbols is non-empty and every member of that set is reported dead. In TypeScript an ambient declaration, one a declaration file holds or one a `declare` modifier carries or a declaration carrying one holds, describes code the target does not implement, so it is no target symbol of that set. A test that references at least one live target symbol is never reported, no notion of a test's subject and no name matching enters the rule, and the message states the rule. A production declaration whose reachability class is `possible` because programs outside the analysis may use it, being published API of a library whose run declares no consumer, and in Go a declaration of a package no build links or an export of a unit a dropped module requires, counts as a live target symbol of every test that references it, so such a test is no test of dead code. The declaration is still reported at `possible`. The test joins the dead component of the symbols it references, and it and each of them count as referencing each other, so the test is a member of their cycle and never a dead symbol outside it.",
|
|
91
91
|
defaultSeverity: "deny",
|
|
92
92
|
maxClass: "certain",
|
|
93
93
|
fixability: "deletable",
|
|
@@ -146,7 +146,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
146
146
|
languages: ["ts"],
|
|
147
147
|
rule: "An exported declaration used only inside its own file, reported as a candidate for removing the export keyword. The finding names the narrower visibility the references support.",
|
|
148
148
|
precondition:
|
|
149
|
-
'Closed world only. Reported in a file that is not an entry file, a file only a configuration string or a script token names being none, because each runs the file and reads no name it exports, and a test file being one only for the exports its test runner calls, as grammar/analysis.md "Test code" states, and that either belongs to a project whose consumer set the configuration declares complete or is reached by no manifest export. A declared cross-language edge counts as a reference from outside the file. Where the edge\'s other side is unknown to the analyzer, the finding is emitted pending. 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.',
|
|
149
|
+
'Closed world only. Reported in a file that is not an entry file, a file only a configuration string or a script token names being none, because each runs the file and reads no name it exports, while a file a build script\'s bundler call names is one, because the bundle exposes what it exports, and a test file being one only for the exports its test runner calls, as grammar/analysis.md "Test code" states, and a file a Markdown or MDX document imports one only for the exports the import reads, as grammar/analysis.md "Markdown and MDX documents" states, and that either belongs to a project whose consumer set the configuration declares complete or is reached by no manifest export. A declared cross-language edge counts as a reference from outside the file. Where the edge\'s other side is unknown to the analyzer, the finding is emitted pending. 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.',
|
|
150
150
|
defaultSeverity: "warn",
|
|
151
151
|
maxClass: "certain",
|
|
152
152
|
fixability: "narrowable",
|
|
@@ -212,9 +212,9 @@ const ROWS: readonly KindRow[] = [
|
|
|
212
212
|
code: "DS1303",
|
|
213
213
|
name: "unused-type-parameter",
|
|
214
214
|
languages: ["go", "ts"],
|
|
215
|
-
rule: "A type parameter of a function
|
|
215
|
+
rule: "A type parameter of a function, a method or a TypeScript type alias that no part of the declaration's signature, body or aliased type names, at the certain class. The deletion is local to the declaration and the explicit instantiations the reference set already lists. A type parameter of a declaration that is itself dead falls with it and is reported under no code of its own.",
|
|
216
216
|
precondition:
|
|
217
|
-
"Function and method type parameters
|
|
217
|
+
"Function and method type parameters, and in TypeScript those of a type alias. A TypeScript alias names no type of its own, so two instantiations that differ only in a parameter its type never names are one type. A Go type declaration's type parameter is never reported, because a phantom type parameter such as `type ID[T any] int` makes two instantiations distinct types while naming the parameter nowhere, so deleting it changes the program. Nor is one of a TypeScript class or interface, which merges only with declarations of the same type parameters. A method signature an interface or an object type declares belongs to that type declaration, so its type parameters are never reported either.",
|
|
218
218
|
defaultSeverity: "deny",
|
|
219
219
|
maxClass: "certain",
|
|
220
220
|
fixability: "deletable",
|
|
@@ -332,7 +332,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
332
332
|
languages: ["go", "ts"],
|
|
333
333
|
rule: "A parameter with no reference inside its function body, on a function whose signature is free to change.",
|
|
334
334
|
precondition:
|
|
335
|
-
"The signature must be free, defined as follows: the function is not a method retained by interface satisfaction, is not a method whose name and signature are those of a method of an exported interface type declared by a package outside the target that the import closure holds, is not used as a value, is not a go:linkname or cgo target, is not a go-fuzz entry point, 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 function literal or function expression that initializes a variable the program only calls, never passing, storing, returning or assigning it, is called by name and is not used as a value. In Go a function literal that is the callee of the call it is written in, the call of a `go` or a `defer` statement included, is not used as a value, wherever the literal is written. 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. In TypeScript a caller may pass a function used as a value more arguments than it declares parameters, so in such a function whose signature is free but for that use, a parameter its body never reads is reported for deletion when its body reads no parameter after it, and an unread parameter before the last read one is reported for a rename to its name with a leading underscore, which keeps every argument in place and which the compilers' unused-parameter rules read as declared unread, unless its name already starts with one. A parameter of a TypeScript function expression or arrow function that the compiler gives a contextual type, and that carries a type annotation, is not reported for deletion where the inference of the call holding the function may read the annotation, being where the callee is generic and its parameter type at the function's place names a type parameter at the parameter's place, or where the function is written inside an object literal such a call is passed, and it is reported for deletion at `probable` where it carries an annotation no such inference reads, because nothing in the program shows that an outside reader ignores it.
|
|
335
|
+
"The signature must be free, defined as follows: the function is not a method retained by interface satisfaction, is not a method whose name and signature are those of a method of an exported interface type declared by a package outside the target that the import closure holds, is not used as a value, is not a go:linkname or cgo target, is not a go-fuzz entry point, 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 function literal or function expression that initializes a variable the program only calls, never passing, storing, returning or assigning it, is called by name and is not used as a value. In Go a function literal that is the callee of the call it is written in, the call of a `go` or a `defer` statement included, is not used as a value, wherever the literal is written. 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. In TypeScript a caller may pass a function used as a value more arguments than it declares parameters, so in such a function whose signature is free but for that use, a parameter its body never reads is reported for deletion when its body reads no parameter after it, and an unread parameter before the last read one is reported for a rename to its name with a leading underscore, which keeps every argument in place and which the compilers' unused-parameter rules read as declared unread, unless its name already starts with one. A parameter of a TypeScript function expression or arrow function that the compiler gives a contextual type, and that carries a type annotation, is not reported for deletion where the inference of the call holding the function may read the annotation, being where the callee is generic and its parameter type at the function's place names a type parameter at the parameter's place, or where the function is written inside an object literal such a call is passed, and it is reported for deletion at `probable` where it carries an annotation no such inference reads, because nothing in the program shows that an outside reader ignores it. A name an object binding pattern of a parameter of such a function binds, annotated or not, that the body never reads is reported for deletion at `probable`, because the pattern reads the property it names from the argument and the code that hands the argument may act on that read, which nothing in the program shows. Any other TypeScript parameter that is a binding pattern is judged by the names it binds: each name the body never reads is reported wherever the pattern stands, because removing it from the pattern moves no argument, except a name beside a rest element of the same object pattern, whose removal changes what that element holds, and the pattern counts as a read parameter when the body reads any name it binds. A rest parameter is judged as the last parameter. A TypeScript `this` parameter declares the type of `this` and receives no argument, so it is never reported. A TypeScript constructor is used as a value where its class is used as a value other than as the callee of a `new` expression or the class an `extends` clause names, and its signature is not free where its class carries a decorator or a parameter of the constructor carries one, because the code that decorator or a container runs may construct the class. A parameter property is read by the write to its member the constructor makes, so it is never reported under this kind, and its member is judged under DS1301. A TypeScript name a parameter binds, whole or through a binding pattern, that starts with an underscore is never reported, because a leading underscore is the spelling unused-parameter rules read as declared unread, and no other name exempts a parameter. A Go parameter named by the blank identifier declares no name a body could read and is never reported.",
|
|
336
336
|
defaultSeverity: "warn",
|
|
337
337
|
maxClass: "certain",
|
|
338
338
|
fixability: "manual",
|
|
@@ -358,7 +358,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
358
358
|
languages: ["go", "ts"],
|
|
359
359
|
rule: "A result value that no call site of the function uses, on a function whose signature is free to change.",
|
|
360
360
|
precondition:
|
|
361
|
-
"The same free-signature rule as unused-parameter, plus a closed-world condition: every call site of the function must be in the loaded graph, because an unknown caller may consume the result, so the kind reports only for a function whose callers are all visible. A call that is the operand of an await expression written as a statement of its own uses a result whose type has a member named then, a promise or another thenable, because the caller sequences on it, and discards any other result. A call written as a statement of its own, or as the operand of void, discards its result whatever its type.",
|
|
361
|
+
"The same free-signature rule as unused-parameter, plus a closed-world condition: every call site of the function must be in the loaded graph, because an unknown caller may consume the result, so the kind reports only for a function whose callers are all visible. A function a program run by naming the file names, a file under the `ignore` tag or a script of a dropped module, has call sites outside the loaded graph. A call that is the operand of an await expression written as a statement of its own uses a result whose type has a member named then, a promise or another thenable, because the caller sequences on it, and discards any other result. A call written as a statement of its own, or as the operand of void, discards its result whatever its type.",
|
|
362
362
|
defaultSeverity: "warn",
|
|
363
363
|
maxClass: "certain",
|
|
364
364
|
fixability: "manual",
|
|
@@ -382,7 +382,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
382
382
|
languages: ["go", "ts"],
|
|
383
383
|
rule: "A write to a local variable with no read before the next write to it or the end of its scope, computed on the same read-and-write classification the write-only-symbol kind uses. The finding names the write position.",
|
|
384
384
|
precondition:
|
|
385
|
-
"In TypeScript the binding of a catch clause is a local variable the clause writes when it catches, so a binding that is a name and that the clause's block never reads is a dead store, positioned at the binding: the clause written with no binding behaves the same. A TypeScript local is a name a `let` or `const` declaration of a block binds, or a `var` declaration of a block of a function, a name an object or array binding pattern of one binds at any depth included, except a name with a default, whose removal skips the default's evaluation, and a name beside a rest element of its object pattern, whose removal changes what the rest holds. A `var` of a block nested in its function's body is judged in that block, its last write kept because the function scope reads on past the block's end, and is no subject where a nested function of its function names it. A local a nested function reads is no subject, because the function may read it at any time, and every write to a local nothing reads is dead, one a nested function makes included, each positioned at the name it writes. The variable a `let` or `const` declaration of a `for…of` or `for…in` statement binds is written at each iteration, so one the loop body never reads is a dead store positioned at the binding, except a name that starts with an underscore, because a loop must bind a name and that spelling declares it unread. In Go a function literal a package-level variable's initializer holds is a function body of its own, and a finding about one of its stores carries the reference of the variable the literal initializes, which for a blank variable is its package's, as `grammar/symbol-ref.md` states for a blank-identifier declaration. In Go a range statement stores its key and its value at the start of every iteration, and an empty range stores neither. In Go a function literal reads each variable it names where it is written, and a store that a literal naming its variable may precede on some path of the control-flow graph, the literal of an earlier iteration of a loop included, is not dead, because a call of the literal the graph does not order may read it. A store no such literal precedes is judged as any other. A literal that is the callee of the call it is written in, outside a `go` or `defer` statement, runs there, so it reads its variables at that call and precedes no later store.",
|
|
385
|
+
"In TypeScript the binding of a catch clause is a local variable the clause writes when it catches, so a binding that is a name and that the clause's block never reads is a dead store, positioned at the binding: the clause written with no binding behaves the same. A TypeScript local is a name a `let` or `const` declaration of a block binds, or a `var` declaration of a block of a function, a name an object or array binding pattern of one binds at any depth included, except a name with a default, whose removal skips the default's evaluation, and a name beside a rest element of its object pattern, whose removal changes what the rest holds. A function declaration of a block of a function is a TypeScript local too, written with its function when the block starts, wherever it stands in the block, so one that nothing outside its own body reads is a dead store positioned at its name, and a read anywhere else in the block reads it. A `let` or `var` declaration with no initializer writes `undefined`, so one that nothing reads and nothing writes is a dead store positioned at its name. A local whose declaration carries the type annotation `never` is no subject: a write to it is the compiler's exhaustiveness check, which fails to compile once a case goes unhandled, so deleting it changes what the program checks. A `var` of a block nested in its function's body is judged in that block, its last write kept because the function scope reads on past the block's end, and is no subject where a nested function of its function names it. A local a nested function reads is no subject, because the function may read it at any time, and every write to a local nothing reads is dead, one a nested function makes included, each positioned at the name it writes. The variable a `let` or `const` declaration of a `for…of` or `for…in` statement binds is written at each iteration, so one the loop body never reads is a dead store positioned at the binding, except a name that starts with an underscore, because a loop must bind a name and that spelling declares it unread. In Go a function literal a package-level variable's initializer holds is a function body of its own, and a finding about one of its stores carries the reference of the variable the literal initializes, which for a blank variable is its package's, as `grammar/symbol-ref.md` states for a blank-identifier declaration. In Go a range statement stores its key and its value at the start of every iteration, and an empty range stores neither. In Go a function literal reads each variable it names where it is written, and a store that a literal naming its variable may precede on some path of the control-flow graph, the literal of an earlier iteration of a loop included, is not dead, because a call of the literal the graph does not order may read it. A store no such literal precedes is judged as any other. A literal that is the callee of the call it is written in, outside a `go` or `defer` statement, runs there, so it reads its variables at that call and precedes no later store.",
|
|
386
386
|
defaultSeverity: "deny",
|
|
387
387
|
maxClass: "certain",
|
|
388
388
|
fixability: "deletable",
|
package/src/manifest.ts
CHANGED
|
@@ -35,8 +35,8 @@ export interface ManifestEntry {
|
|
|
35
35
|
readonly path: string;
|
|
36
36
|
readonly role: EntryRole;
|
|
37
37
|
/**
|
|
38
|
-
* Whether the member declares what a consumer may import, which is `exports`
|
|
39
|
-
* the three members that preceded
|
|
38
|
+
* Whether the member declares what a consumer may import, which is `exports`, `browser`
|
|
39
|
+
* and the three members that preceded `exports`. A command is published too, and is not this:
|
|
40
40
|
* `exports` is the surface a library's published API is read from.
|
|
41
41
|
*/
|
|
42
42
|
readonly published: boolean;
|
|
@@ -136,7 +136,7 @@ function manifestOf(host: Host, targetRoot: string): Record<string, unknown> | u
|
|
|
136
136
|
export interface Manifest {
|
|
137
137
|
/**
|
|
138
138
|
* Every entry point the manifest names, in the order the members are read: `main`,
|
|
139
|
-
* `module`, `types`, then each `bin` command, then every target of `exports`, then
|
|
139
|
+
* `module`, `types`, then `browser` and each file it maps a module to, then each `bin` command, then every target of `exports`, then
|
|
140
140
|
* every token of each script, which names a file only where one is at its path. Each
|
|
141
141
|
* path is absolute.
|
|
142
142
|
*/
|
|
@@ -171,6 +171,24 @@ export function readManifest(host: Host, targetRoot: string): Manifest {
|
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
// A `browser` string replaces `main` in a browser bundle, and an object replaces each
|
|
175
|
+
// module it maps with the file it names: https://github.com/defunctzombie/package-browser-field-spec
|
|
176
|
+
const browser = manifest["browser"];
|
|
177
|
+
if (typeof browser === "string" && isTarget(browser, false)) {
|
|
178
|
+
found.push({ member: "browser", path: browser, role: "import", published: true });
|
|
179
|
+
} else if (isRecord(browser)) {
|
|
180
|
+
for (const [replaced, value] of Object.entries(browser)) {
|
|
181
|
+
if (typeof value === "string" && isTarget(value, true)) {
|
|
182
|
+
found.push({
|
|
183
|
+
member: below("browser", replaced),
|
|
184
|
+
path: value,
|
|
185
|
+
role: "import",
|
|
186
|
+
published: true,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
174
192
|
const bin = manifest["bin"];
|
|
175
193
|
if (typeof bin === "string" && isTarget(bin, false)) {
|
|
176
194
|
found.push({ member: "bin", path: bin, role: "run", published: false });
|
package/src/matrix.ts
CHANGED
|
@@ -30,6 +30,8 @@ export interface Configured {
|
|
|
30
30
|
readonly testFiles: readonly string[];
|
|
31
31
|
/** The paths of the test-support files, which only test code imports. */
|
|
32
32
|
readonly supportFiles?: readonly string[] | undefined;
|
|
33
|
+
/** The paths of the files only tests reach that are no test-support file. */
|
|
34
|
+
readonly testUsedFiles?: readonly string[] | undefined;
|
|
33
35
|
/** The ids of the ambient declarations, which describe code the target does not implement. */
|
|
34
36
|
readonly ambient?: ReadonlySet<string> | undefined;
|
|
35
37
|
/**
|
|
@@ -60,6 +62,11 @@ export interface Matrix {
|
|
|
60
62
|
* whose project holds it, in the run's order.
|
|
61
63
|
*/
|
|
62
64
|
readonly heldIn: readonly (readonly string[])[];
|
|
65
|
+
/**
|
|
66
|
+
* Per declaration of {@link Matrix.union}, whether a file only tests reach that is no
|
|
67
|
+
* test-support file holds it in some configuration: production code only tests use.
|
|
68
|
+
*/
|
|
69
|
+
readonly testUsed: readonly boolean[];
|
|
63
70
|
/** Each declaration's dependents, by id, as the first configuration holding it states them. */
|
|
64
71
|
readonly dependents: ReadonlyMap<string, readonly string[]>;
|
|
65
72
|
}
|
|
@@ -81,6 +88,17 @@ export interface MatrixCandidate extends Candidate {
|
|
|
81
88
|
* shape nothing references would be.
|
|
82
89
|
*/
|
|
83
90
|
readonly unreferencedTest: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Whether it is dead only because code only tests use is: production code only tests
|
|
93
|
+
* use holds it and the run that counts test references reaches it, or such a
|
|
94
|
+
* declaration reaches it through dead declarations.
|
|
95
|
+
*/
|
|
96
|
+
readonly testUsed: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Whether test code the run that counts test references reaches, and that is no test of
|
|
99
|
+
* dead code, references it: a test that stays keeps it.
|
|
100
|
+
*/
|
|
101
|
+
readonly keptByTests: boolean;
|
|
84
102
|
}
|
|
85
103
|
|
|
86
104
|
/** One run's sweep. */
|
|
@@ -158,6 +176,7 @@ export function matrixOf(per: readonly Configured[]): Matrix {
|
|
|
158
176
|
}
|
|
159
177
|
});
|
|
160
178
|
symbols.sort((a, b) => byPosition(a.position, b.position));
|
|
179
|
+
const testUsed = new Set(per.flatMap((one) => one.testUsedFiles ?? []));
|
|
161
180
|
|
|
162
181
|
return {
|
|
163
182
|
configurations: per.map((one) => one.configuration),
|
|
@@ -173,6 +192,7 @@ export function matrixOf(per: readonly Configured[]): Matrix {
|
|
|
173
192
|
new Set(per.flatMap((one) => [...(one.ambient ?? [])])),
|
|
174
193
|
),
|
|
175
194
|
heldIn: symbols.map((symbol) => holders.get(symbol.id) ?? []),
|
|
195
|
+
testUsed: symbols.map((symbol) => testUsed.has(symbol.position.path)),
|
|
176
196
|
dependents,
|
|
177
197
|
};
|
|
178
198
|
}
|
|
@@ -203,18 +223,32 @@ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
|
|
|
203
223
|
|
|
204
224
|
const union = matrix.union;
|
|
205
225
|
const heldBy: (string | undefined)[] = union.symbols.map(() => undefined);
|
|
206
|
-
const
|
|
226
|
+
const held = [
|
|
207
227
|
...input.marked,
|
|
208
228
|
...(input.exempt ?? []).map((record) => record.id),
|
|
209
229
|
...(input.unanswered ?? []),
|
|
210
|
-
]
|
|
211
|
-
const
|
|
230
|
+
];
|
|
231
|
+
const testReached = reachedWithTheTests(union, held);
|
|
232
|
+
// A test uses what it names: a file a test reads through a star re-export or a
|
|
233
|
+
// namespace keeps the exports nothing names at their own class.
|
|
234
|
+
const testNamed = reachedWithTheTests(union, held, false);
|
|
235
|
+
const everywhere = union.symbols.map((symbol, at): readonly Candidate[] | undefined => {
|
|
212
236
|
const held = matrix.heldIn[at] ?? [];
|
|
213
237
|
const found = held.map((configuration) => byConfiguration.get(configuration)?.get(symbol.id));
|
|
214
238
|
return held.length === 0 || found.some((candidate) => candidate === undefined)
|
|
215
239
|
? undefined
|
|
216
240
|
: found.filter((candidate): candidate is Candidate => candidate !== undefined);
|
|
217
241
|
});
|
|
242
|
+
const compiled = compiledReferences(
|
|
243
|
+
matrix,
|
|
244
|
+
new Set(
|
|
245
|
+
union.symbols.flatMap((symbol, at) => (everywhere[at] === undefined ? [] : [symbol.id])),
|
|
246
|
+
),
|
|
247
|
+
input.mode,
|
|
248
|
+
);
|
|
249
|
+
const judged = everywhere.map((found, at) =>
|
|
250
|
+
compiled.has(union.symbols[at]?.id ?? "") ? undefined : found,
|
|
251
|
+
);
|
|
218
252
|
// A configuration that does not hold a test's subject's caller finds the subject dead,
|
|
219
253
|
// so a test is of dead code only where every production subject it names is dead in
|
|
220
254
|
// every configuration that holds it.
|
|
@@ -227,6 +261,14 @@ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
|
|
|
227
261
|
union.subject[edge.to] !== true ||
|
|
228
262
|
judged[edge.to] !== undefined,
|
|
229
263
|
);
|
|
264
|
+
const admittedAt = (at: number): boolean =>
|
|
265
|
+
judged[at]?.every((candidate) => candidate.testOfDeadCode) === true && subjectsDead(at);
|
|
266
|
+
const kept = keptByTests(union, testReached, admittedAt);
|
|
267
|
+
const testUsed = testUsedReach(
|
|
268
|
+
union,
|
|
269
|
+
union.symbols.map((_symbol, at) => matrix.testUsed[at] === true && testNamed[at] === true),
|
|
270
|
+
judged.map((one) => one !== undefined),
|
|
271
|
+
);
|
|
230
272
|
const candidates: MatrixCandidate[] = [];
|
|
231
273
|
union.symbols.forEach((symbol, at) => {
|
|
232
274
|
const all = judged[at];
|
|
@@ -234,7 +276,7 @@ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
|
|
|
234
276
|
return;
|
|
235
277
|
}
|
|
236
278
|
const held = matrix.heldIn[at] ?? [];
|
|
237
|
-
const admitted =
|
|
279
|
+
const admitted = admittedAt(at);
|
|
238
280
|
const testRefs = union.made[at]?.test ?? 0;
|
|
239
281
|
const heldByTests =
|
|
240
282
|
heldBy[union.parent[at] ?? OUTSIDE] ??
|
|
@@ -255,6 +297,8 @@ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
|
|
|
255
297
|
configurations: held,
|
|
256
298
|
heldByTests,
|
|
257
299
|
unreferencedTest: unreferenced,
|
|
300
|
+
testUsed: testUsed[at] === true,
|
|
301
|
+
keptByTests: kept[at] === true,
|
|
258
302
|
});
|
|
259
303
|
});
|
|
260
304
|
|
|
@@ -266,6 +310,80 @@ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
|
|
|
266
310
|
};
|
|
267
311
|
}
|
|
268
312
|
|
|
313
|
+
/**
|
|
314
|
+
* Every declaration of `dead`, those dead in every configuration that holds them, that a
|
|
315
|
+
* configuration's graph references from a judged declaration outside `dead` or already
|
|
316
|
+
* kept, until none joins: deleting one would break that configuration's compile. A
|
|
317
|
+
* production run counts no reference a test file made.
|
|
318
|
+
*/
|
|
319
|
+
function compiledReferences(
|
|
320
|
+
matrix: Matrix,
|
|
321
|
+
dead: ReadonlySet<string>,
|
|
322
|
+
mode: SweepInput["mode"],
|
|
323
|
+
): ReadonlySet<string> {
|
|
324
|
+
const kept = new Set<string>();
|
|
325
|
+
for (let changed = true; changed;) {
|
|
326
|
+
changed = false;
|
|
327
|
+
for (const graph of matrix.graphs) {
|
|
328
|
+
graph.out.forEach((edges, from) => {
|
|
329
|
+
const id = graph.symbols[from]?.id ?? "";
|
|
330
|
+
if (graph.subject[from] !== true || (dead.has(id) && !kept.has(id))) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
for (const edge of edges) {
|
|
334
|
+
const to = graph.symbols[edge.to]?.id ?? "";
|
|
335
|
+
if ((!mode.production || !edge.test) && dead.has(to) && !kept.has(to)) {
|
|
336
|
+
kept.add(to);
|
|
337
|
+
changed = true;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return kept;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Per declaration of the union, whether one of `seeds` is it or reaches it through `dead` declarations. */
|
|
347
|
+
function testUsedReach(
|
|
348
|
+
union: Graph,
|
|
349
|
+
seeds: readonly boolean[],
|
|
350
|
+
dead: readonly boolean[],
|
|
351
|
+
): readonly boolean[] {
|
|
352
|
+
// A file is no candidate, and its top level is what reaches what it alone calls.
|
|
353
|
+
const reached = seeds.map((seed, at) => seed && dead[at] === true);
|
|
354
|
+
const queue = seeds.flatMap((one, at) => (one ? [at] : []));
|
|
355
|
+
for (let at = queue.pop(); at !== undefined; at = queue.pop()) {
|
|
356
|
+
for (const edge of union.out[at] ?? []) {
|
|
357
|
+
if (dead[edge.to] === true && reached[edge.to] !== true) {
|
|
358
|
+
reached[edge.to] = true;
|
|
359
|
+
queue.push(edge.to);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return reached;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Per declaration of the union, whether test code `reached` holds and `admitted` rejects references it. */
|
|
367
|
+
function keptByTests(
|
|
368
|
+
union: Graph,
|
|
369
|
+
reached: readonly boolean[],
|
|
370
|
+
admitted: (at: number) => boolean,
|
|
371
|
+
): readonly boolean[] {
|
|
372
|
+
const kept = union.symbols.map(() => false);
|
|
373
|
+
union.out.forEach((edges, from) => {
|
|
374
|
+
const testCode = union.test[from] === true || union.support[from] === true;
|
|
375
|
+
if (!testCode || reached[from] !== true || admitted(from)) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
for (const edge of edges) {
|
|
379
|
+
if (edge.test) {
|
|
380
|
+
kept[edge.to] = true;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
return kept;
|
|
385
|
+
}
|
|
386
|
+
|
|
269
387
|
/**
|
|
270
388
|
* The dead components of one sweep over the union, with the file declarations at `files`
|
|
271
389
|
* dead as well: a file that is deleted whole takes the declarations it holds and every
|