@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/src/conventions.ts
CHANGED
|
@@ -66,6 +66,7 @@ import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from
|
|
|
66
66
|
import type { SetupFailure } from "./setup-failure.ts";
|
|
67
67
|
import type { SourceFiles } from "./source-files.ts";
|
|
68
68
|
import { satisfies } from "./version-range.ts";
|
|
69
|
+
import { yamlDocument } from "./yaml-document.ts";
|
|
69
70
|
|
|
70
71
|
/** One manifest the analysis reads. */
|
|
71
72
|
export interface ConventionManifest {
|
|
@@ -151,6 +152,13 @@ type ParseSource = (fileName: string, text: string) => SourceFile;
|
|
|
151
152
|
|
|
152
153
|
const JSON_EXTENSION = ".json";
|
|
153
154
|
|
|
155
|
+
/**
|
|
156
|
+
* A configuration file its tool reads as a YAML document, read as the JSON document it
|
|
157
|
+
* denotes. Its nodes' offsets are that JSON text's, so no move may read one: a failure's
|
|
158
|
+
* line would name the wrong line.
|
|
159
|
+
*/
|
|
160
|
+
const YAML_NAME = /\.ya?ml$/u;
|
|
161
|
+
|
|
154
162
|
/** A configuration file with no extension that its tool reads as a JSON document. */
|
|
155
163
|
const JSON_RC = /^\.[\w-]+rc$/u;
|
|
156
164
|
|
|
@@ -883,6 +891,14 @@ function readingFiles(
|
|
|
883
891
|
} catch {
|
|
884
892
|
continue;
|
|
885
893
|
}
|
|
894
|
+
if (YAML_NAME.test(name)) {
|
|
895
|
+
const value = yamlDocument(text);
|
|
896
|
+
if (value !== undefined) {
|
|
897
|
+
const json = JSON.stringify(value);
|
|
898
|
+
found.push({ name, text: json, file: parse(`${path}${JSON_EXTENSION}`, json) });
|
|
899
|
+
}
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
886
902
|
found.push({
|
|
887
903
|
name,
|
|
888
904
|
text,
|
package/src/dependencies.ts
CHANGED
|
@@ -871,6 +871,98 @@ function scriptTokensOf(manifest: Record<string, unknown>): ReadonlySet<string>
|
|
|
871
871
|
return found;
|
|
872
872
|
}
|
|
873
873
|
|
|
874
|
+
/**
|
|
875
|
+
* The runtime flags whose value is a module loaded before the entry, as `--flag value` or
|
|
876
|
+
* `--flag=value`: https://nodejs.org/api/cli.html#--importmodule,
|
|
877
|
+
* https://nodejs.org/api/cli.html#--experimental-loadermodule,
|
|
878
|
+
* https://nodejs.org/api/cli.html#-r---require-module
|
|
879
|
+
*/
|
|
880
|
+
const PRELOAD_FLAGS: ReadonlySet<string> = new Set([
|
|
881
|
+
"--experimental-loader",
|
|
882
|
+
"--import",
|
|
883
|
+
"--loader",
|
|
884
|
+
"--require",
|
|
885
|
+
"-r",
|
|
886
|
+
]);
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* The programs that pass a preload flag to the runtime they start: Node itself, and the
|
|
890
|
+
* runners that forward Node's flags or read the same `--require`:
|
|
891
|
+
* https://mochajs.org/#-require-module-r-module, https://tsx.is/node-enhancement,
|
|
892
|
+
* https://typestrong.org/ts-node/docs/options#require,
|
|
893
|
+
* https://github.com/remy/nodemon#running-non-node-scripts
|
|
894
|
+
*/
|
|
895
|
+
const PRELOADING_PROGRAMS: ReadonlySet<string> = new Set([
|
|
896
|
+
"mocha",
|
|
897
|
+
"node",
|
|
898
|
+
"nodemon",
|
|
899
|
+
"ts-node",
|
|
900
|
+
"tsx",
|
|
901
|
+
]);
|
|
902
|
+
|
|
903
|
+
/** The programs that run the command after them, and an environment assignment before it. */
|
|
904
|
+
const LAUNCHERS: ReadonlySet<string> = new Set(["cross-env", "env", "npx"]);
|
|
905
|
+
const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/u;
|
|
906
|
+
|
|
907
|
+
/** The commands one script string runs, split at the shell's list and pipeline operators. */
|
|
908
|
+
const COMMAND_SEPARATORS = /&&|\|\||;|\||&/u;
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* The packages one manifest's scripts preload by a runtime flag's bare module specifier,
|
|
912
|
+
* a flag counting only in a command whose program passes it to the runtime, and for Node
|
|
913
|
+
* only before the script it runs, whose own arguments come after it.
|
|
914
|
+
*/
|
|
915
|
+
function scriptPreloadsOf(manifest: Record<string, unknown>): ReadonlySet<string> {
|
|
916
|
+
const scripts = manifest["scripts"];
|
|
917
|
+
const found = new Set<string>();
|
|
918
|
+
if (!isRecord(scripts)) {
|
|
919
|
+
return found;
|
|
920
|
+
}
|
|
921
|
+
for (const script of Object.values(scripts)) {
|
|
922
|
+
if (typeof script !== "string") {
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
for (const command of script.split(COMMAND_SEPARATORS)) {
|
|
926
|
+
const tokens = scriptTokens(command);
|
|
927
|
+
let at = 0;
|
|
928
|
+
while (
|
|
929
|
+
at < tokens.length &&
|
|
930
|
+
(ASSIGNMENT.test(tokens[at] ?? "") || LAUNCHERS.has(tokens[at] ?? ""))
|
|
931
|
+
) {
|
|
932
|
+
at += 1;
|
|
933
|
+
}
|
|
934
|
+
const path = tokens[at] ?? "";
|
|
935
|
+
const program = path.slice(path.lastIndexOf("/") + 1);
|
|
936
|
+
if (!PRELOADING_PROGRAMS.has(program)) {
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
for (at += 1; at < tokens.length; at += 1) {
|
|
940
|
+
const token = tokens[at] ?? "";
|
|
941
|
+
if (!token.startsWith("-")) {
|
|
942
|
+
if (program === "node") {
|
|
943
|
+
break;
|
|
944
|
+
}
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
const equals = token.indexOf("=");
|
|
948
|
+
const flag = equals < 0 ? token : token.slice(0, equals);
|
|
949
|
+
if (!PRELOAD_FLAGS.has(flag)) {
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
if (equals < 0) {
|
|
953
|
+
at += 1;
|
|
954
|
+
}
|
|
955
|
+
const value = equals < 0 ? tokens[at] : token.slice(equals + 1);
|
|
956
|
+
const name = value === undefined ? undefined : packageOfSpecifier(value);
|
|
957
|
+
if (name !== undefined) {
|
|
958
|
+
found.add(name);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
return found;
|
|
964
|
+
}
|
|
965
|
+
|
|
874
966
|
/** Whether a path below the target root lies in the directory `dir` below it, `""` being the root. */
|
|
875
967
|
function isInside(path: string, dir: string): boolean {
|
|
876
968
|
return dir === "" || path.startsWith(`${dir}/`);
|
|
@@ -976,12 +1068,16 @@ export function dependenciesOf(
|
|
|
976
1068
|
continue;
|
|
977
1069
|
}
|
|
978
1070
|
const tokens = scriptTokensOf(read.value);
|
|
1071
|
+
const preloads = scriptPreloadsOf(read.value);
|
|
979
1072
|
for (const dependency of declaredIn(read.text, read.value, path, scopeOf(path))) {
|
|
980
1073
|
const found = installedOf(host, dir, dependency.name);
|
|
981
1074
|
if (found === undefined && uninstalledHere.has(dir)) {
|
|
982
1075
|
continue;
|
|
983
1076
|
}
|
|
984
1077
|
declared.push(dependency);
|
|
1078
|
+
if (preloads.has(dependency.name)) {
|
|
1079
|
+
ran.add(declarationKey(dependency));
|
|
1080
|
+
}
|
|
985
1081
|
if (found === undefined) {
|
|
986
1082
|
continue;
|
|
987
1083
|
}
|
package/src/entry-points.ts
CHANGED
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
import {
|
|
15
15
|
isArrayLiteralExpression,
|
|
16
16
|
isAsExpression,
|
|
17
|
-
isBinaryExpression,
|
|
18
17
|
isCallExpression,
|
|
19
18
|
isExportAssignment,
|
|
20
19
|
isIdentifier,
|
|
@@ -27,7 +26,6 @@ import {
|
|
|
27
26
|
isPropertyAssignment,
|
|
28
27
|
isSatisfiesExpression,
|
|
29
28
|
isStringLiteral,
|
|
30
|
-
isTemplateExpression,
|
|
31
29
|
SyntaxKind,
|
|
32
30
|
type Node,
|
|
33
31
|
type SourceFile,
|
|
@@ -40,6 +38,7 @@ import {
|
|
|
40
38
|
type ParseFile,
|
|
41
39
|
} from "./configuration-files.ts";
|
|
42
40
|
import type { Selected } from "./conventions.ts";
|
|
41
|
+
import { buildScriptEntries } from "./build-scripts.ts";
|
|
43
42
|
import { globExpression } from "./glob.ts";
|
|
44
43
|
import type { Host } from "./host.ts";
|
|
45
44
|
import { htmlEntries } from "./html-entries.ts";
|
|
@@ -48,6 +47,7 @@ import { dirnamePath, relativePath, resolvePath } from "./paths.ts";
|
|
|
48
47
|
import { UNANSWERED } from "./query.ts";
|
|
49
48
|
import type { ProjectView } from "./session.ts";
|
|
50
49
|
import type { SourceFiles } from "./source-files.ts";
|
|
50
|
+
import { computedLoadDirectory, leadingLiteral } from "./unseen-loads.ts";
|
|
51
51
|
import { workflowEntries } from "./workflow-steps.ts";
|
|
52
52
|
|
|
53
53
|
/** Which rule entered a file. */
|
|
@@ -56,6 +56,8 @@ export type EntryRule =
|
|
|
56
56
|
| "configuration-file"
|
|
57
57
|
/** A file a relative string of a configuration module or a JSON configuration file names. */
|
|
58
58
|
| "configuration-string"
|
|
59
|
+
/** An entry a build script's bundler call names, whose exports the bundle exposes. */
|
|
60
|
+
| "bundler-entry"
|
|
59
61
|
/** The test runner: its workspace configuration, its setup files and the test files. */
|
|
60
62
|
| "test-runner"
|
|
61
63
|
/** The test files below the browser-test runner's test directory. */
|
|
@@ -324,10 +326,7 @@ function configurationEntries(
|
|
|
324
326
|
strings(dirnamePath(document.path), unselectedIn(document.path, document.strings));
|
|
325
327
|
}
|
|
326
328
|
for (const { file } of outside) {
|
|
327
|
-
strings(
|
|
328
|
-
dirnamePath(file.fileName),
|
|
329
|
-
moduleStrings(file).map((one) => one.text),
|
|
330
|
-
);
|
|
329
|
+
strings(dirnamePath(file.fileName), unselected(file));
|
|
331
330
|
}
|
|
332
331
|
return found;
|
|
333
332
|
}
|
|
@@ -541,18 +540,6 @@ function workersOf<Brand>(
|
|
|
541
540
|
return found;
|
|
542
541
|
}
|
|
543
542
|
|
|
544
|
-
/** The literal text a computed path starts with: a template's head, or a concatenation's first literal. */
|
|
545
|
-
function leadingLiteral(node: Node): string | undefined {
|
|
546
|
-
const at = unwrapped(node);
|
|
547
|
-
if (isTemplateExpression(at)) {
|
|
548
|
-
return at.head.text;
|
|
549
|
-
}
|
|
550
|
-
if (isBinaryExpression(at) && at.operatorToken.kind === SyntaxKind.PlusToken) {
|
|
551
|
-
return literalOf(unwrapped(at.left)) ?? leadingLiteral(at.left);
|
|
552
|
-
}
|
|
553
|
-
return undefined;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
543
|
/** Whether one call is `import.meta.glob(...)`. */
|
|
557
544
|
function isMetaGlob(node: Node): boolean {
|
|
558
545
|
if (!isCallExpression(node) || !isPropertyAccessExpression(node.expression)) {
|
|
@@ -579,13 +566,8 @@ function computedImportsOf(file: SourceFile, files: SourceFiles): readonly Entry
|
|
|
579
566
|
const dir = dirnamePath(file.fileName);
|
|
580
567
|
const found: EntryPoint[] = [];
|
|
581
568
|
const below = (prefix: string): void => {
|
|
582
|
-
const target =
|
|
583
|
-
if (
|
|
584
|
-
!isModuleRelative(prefix) ||
|
|
585
|
-
!prefix.endsWith("/") ||
|
|
586
|
-
target === dir ||
|
|
587
|
-
relativePath(target, dir) !== undefined
|
|
588
|
-
) {
|
|
569
|
+
const target = computedLoadDirectory(dir, prefix);
|
|
570
|
+
if (target === undefined) {
|
|
589
571
|
return;
|
|
590
572
|
}
|
|
591
573
|
for (const candidate of files.byName.values()) {
|
|
@@ -680,6 +662,9 @@ export function entryPoints<Brand>(
|
|
|
680
662
|
...(names === undefined ? {} : { names }),
|
|
681
663
|
});
|
|
682
664
|
}
|
|
665
|
+
for (const { file, source } of buildScriptEntries(host, targetRoot, files, parse)) {
|
|
666
|
+
found.push({ file, rule: "bundler-entry", source, exports: true });
|
|
667
|
+
}
|
|
683
668
|
for (const { file, source } of workflowEntries(host, targetRoot, files)) {
|
|
684
669
|
found.push({ file, rule: "workflow-step", source, exports: true });
|
|
685
670
|
}
|
package/src/file-facts.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { packageScope } from "./inventory.ts";
|
|
|
11
11
|
import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
|
|
12
12
|
import { renderRef } from "./ref.ts";
|
|
13
13
|
import type { ProjectView } from "./session.ts";
|
|
14
|
+
import type { UnseenLoad } from "./unseen-loads.ts";
|
|
14
15
|
|
|
15
16
|
/** A file name the compiler reads as a source file. */
|
|
16
17
|
const SOURCE_FILE = /\.(?:ts|tsx|mts|cts)$/u;
|
|
@@ -51,6 +52,8 @@ export interface FileFacts {
|
|
|
51
52
|
readonly generated?: ReadonlySet<string>;
|
|
52
53
|
/** The property names an `in` expression of a production file tests by a literal. */
|
|
53
54
|
readonly inOperands?: ReadonlySet<string>;
|
|
55
|
+
/** The loads of production files whose target the analysis cannot resolve. */
|
|
56
|
+
readonly unseenLoads?: readonly UnseenLoad[];
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
/** The line of the last character of one text, counted from one; an empty text is one line. */
|
package/src/finding.ts
CHANGED
|
@@ -119,6 +119,11 @@ export interface CompletedFinding extends Finding {
|
|
|
119
119
|
readonly reachabilityClass: Confidence;
|
|
120
120
|
/** The reachability class capped by the code's ceiling. */
|
|
121
121
|
readonly confidence: Confidence;
|
|
122
|
+
/**
|
|
123
|
+
* Where test-used code alone lowers the confidence, the one the finding would carry
|
|
124
|
+
* otherwise, which its component's cap and the dials read.
|
|
125
|
+
*/
|
|
126
|
+
readonly componentConfidence?: Confidence;
|
|
122
127
|
/**
|
|
123
128
|
* The relation that decided the subject: present on a declaration the sweep judged
|
|
124
129
|
* dead, absent on a live one and on a subject that is no declaration.
|
|
@@ -132,6 +132,34 @@ function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
|
|
|
132
132
|
};
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Whether one test of dead code references a target symbol that is published API of a run
|
|
137
|
+
* that declares no consumer, which outside programs the run cannot see may use, so the
|
|
138
|
+
* target counts as live for the test and the test is no test of dead code.
|
|
139
|
+
*/
|
|
140
|
+
export function testsUnseenAPI(input: EmitterInput): (id: string) => boolean {
|
|
141
|
+
if (input.boundary.consumers.declared.length > 0) {
|
|
142
|
+
return () => false;
|
|
143
|
+
}
|
|
144
|
+
const union = input.swept.matrix.union;
|
|
145
|
+
const published = publishedIds(union);
|
|
146
|
+
return (id) => testTargets(union, id).some((target) => published.has(target));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The target symbols one test of dead code references, by id: production declarations the sweep judges. */
|
|
150
|
+
function testTargets(union: Graph, id: string): readonly string[] {
|
|
151
|
+
return (union.out[union.at(id)] ?? []).flatMap((edge) => {
|
|
152
|
+
const target = union.symbols[edge.to];
|
|
153
|
+
const testCode = union.test[edge.to] === true || union.support[edge.to] === true;
|
|
154
|
+
return target !== undefined &&
|
|
155
|
+
!testCode &&
|
|
156
|
+
union.ambient[edge.to] !== true &&
|
|
157
|
+
union.subject[edge.to] === true
|
|
158
|
+
? [target.id]
|
|
159
|
+
: [];
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
135
163
|
/**
|
|
136
164
|
* The confidence ceiling of a test of dead code: the lowest reachability class among the
|
|
137
165
|
* target symbols it references, because its deadness is theirs, whether or not they share
|
|
@@ -145,40 +173,59 @@ function testCeilings(
|
|
|
145
173
|
const tests = new Set(
|
|
146
174
|
input.swept.sweep.candidates.filter((one) => one.testOfDeadCode).map((one) => one.id),
|
|
147
175
|
);
|
|
148
|
-
return (id) =>
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
176
|
+
return (id) =>
|
|
177
|
+
tests.has(id)
|
|
178
|
+
? testTargets(union, id).reduce<Confidence>(
|
|
179
|
+
(lowest, target) => capped(lowest, classOf(target)),
|
|
180
|
+
"certain",
|
|
181
|
+
)
|
|
182
|
+
: "certain";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The subjects dead only because production code only tests use is: each declaration the
|
|
187
|
+
* sweep marks test-used, each `DS1502` file of such code, and each test of dead code every
|
|
188
|
+
* target of which is one. Nothing they hold references a dead declaration outside them, so
|
|
189
|
+
* the rest of a component they share is reported without them.
|
|
190
|
+
*/
|
|
191
|
+
function testUsedSubjects(input: EmitterInput): {
|
|
192
|
+
readonly declarations: ReadonlySet<string>;
|
|
193
|
+
readonly files: ReadonlySet<string>;
|
|
194
|
+
} {
|
|
195
|
+
const { matrix, sweep } = input.swept;
|
|
196
|
+
const union = matrix.union;
|
|
197
|
+
const used = new Set(sweep.candidates.filter((one) => one.testUsed).map((one) => one.id));
|
|
198
|
+
for (const test of sweep.candidates.filter((one) => one.testOfDeadCode)) {
|
|
199
|
+
const targets = testTargets(union, test.id);
|
|
200
|
+
if (targets.length > 0 && targets.every((target) => used.has(target))) {
|
|
201
|
+
used.add(test.id);
|
|
164
202
|
}
|
|
165
|
-
|
|
166
|
-
|
|
203
|
+
}
|
|
204
|
+
const files = new Set(
|
|
205
|
+
union.symbols.flatMap((symbol, at) =>
|
|
206
|
+
matrix.testUsed[at] === true ? [symbol.position.path] : [],
|
|
207
|
+
),
|
|
208
|
+
);
|
|
209
|
+
return { declarations: used, files };
|
|
167
210
|
}
|
|
168
211
|
|
|
169
212
|
/**
|
|
170
213
|
* The ceiling the roots the run found put on one dead subject: a subject other declarations
|
|
171
|
-
* reference, found dead only because no root reaches them, is `probable` unless
|
|
172
|
-
*
|
|
173
|
-
*
|
|
214
|
+
* reference, found dead only because no root reaches them, is `probable` unless the
|
|
215
|
+
* configuration declares the roots complete and no configuration holding the subject loads
|
|
216
|
+
* a module the analysis cannot resolve, because a root the analysis cannot see would make
|
|
217
|
+
* it live; every other declaration has none below `certain`.
|
|
174
218
|
*/
|
|
175
219
|
function rootsCeiling(
|
|
176
220
|
input: EmitterInput,
|
|
177
221
|
finding: Finding,
|
|
178
222
|
candidate: Candidate | undefined,
|
|
223
|
+
configurations: readonly string[],
|
|
179
224
|
): Confidence {
|
|
180
225
|
const unreachable = candidate?.relation === "reachability" || finding.unreachableOnly === true;
|
|
181
|
-
|
|
226
|
+
const unseen = new Set((input.files.unseenLoads ?? []).map((load) => load.configuration));
|
|
227
|
+
const complete = input.config.ts.rootsComplete && !configurations.some((one) => unseen.has(one));
|
|
228
|
+
return unreachable && !complete ? "probable" : "certain";
|
|
182
229
|
}
|
|
183
230
|
|
|
184
231
|
/**
|
|
@@ -196,9 +243,28 @@ function findingComponents(
|
|
|
196
243
|
held.set(member, component);
|
|
197
244
|
}
|
|
198
245
|
}
|
|
246
|
+
const listed = membersListed(swept);
|
|
247
|
+
return (id) => {
|
|
248
|
+
const component = held.get(id);
|
|
249
|
+
return component === undefined
|
|
250
|
+
? undefined
|
|
251
|
+
: {
|
|
252
|
+
id: component.id,
|
|
253
|
+
root: component.roots.includes(id),
|
|
254
|
+
symbolCount: component.members.length,
|
|
255
|
+
deletableLines: component.deletableLines,
|
|
256
|
+
...(cascade === "full" ? { members: listed(component.members) } : {}),
|
|
257
|
+
};
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** The members of a component as the run lists them, positioned and in the canonical order. */
|
|
262
|
+
function membersListed(
|
|
263
|
+
swept: RunSweep,
|
|
264
|
+
): (members: readonly string[]) => readonly PositionedSymbol[] {
|
|
199
265
|
const symbols = new Map(swept.matrix.union.symbols.map((symbol) => [symbol.id, symbol]));
|
|
200
|
-
|
|
201
|
-
|
|
266
|
+
return (members) =>
|
|
267
|
+
members
|
|
202
268
|
.flatMap((member) => {
|
|
203
269
|
const symbol = symbols.get(member);
|
|
204
270
|
return symbol === undefined
|
|
@@ -215,18 +281,6 @@ function findingComponents(
|
|
|
215
281
|
];
|
|
216
282
|
})
|
|
217
283
|
.sort((a, b) => byPosition(a.position, b.position));
|
|
218
|
-
return (id) => {
|
|
219
|
-
const component = held.get(id);
|
|
220
|
-
return component === undefined
|
|
221
|
-
? undefined
|
|
222
|
-
: {
|
|
223
|
-
id: component.id,
|
|
224
|
-
root: component.roots.includes(id),
|
|
225
|
-
symbolCount: component.members.length,
|
|
226
|
-
deletableLines: component.deletableLines,
|
|
227
|
-
...(cascade === "full" ? { members: listed(component) } : {}),
|
|
228
|
-
};
|
|
229
|
-
};
|
|
230
284
|
}
|
|
231
285
|
|
|
232
286
|
/**
|
|
@@ -321,18 +375,20 @@ export function completed(
|
|
|
321
375
|
const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
|
|
322
376
|
const classOf = reachabilityClasses(input);
|
|
323
377
|
const ceilingOf = testCeilings(input, classOf);
|
|
378
|
+
const testUsed = testUsedSubjects(input);
|
|
324
379
|
const { components, computed, files } = runComponents(swept, findings);
|
|
325
380
|
const componentOf = findingComponents(swept, components, input.config.reporters.cascade);
|
|
326
381
|
const held = new Set(components.filter((one) => one.held).flatMap((one) => one.members));
|
|
327
382
|
// A join keeps its first component's identifier, so a minted one counts past every computed one.
|
|
328
383
|
let minted = computed;
|
|
329
384
|
const dead = new Set(components.map((component) => component.id));
|
|
330
|
-
const fallingWith = new Map<string,
|
|
331
|
-
for (const { heldByTests } of swept.sweep.candidates) {
|
|
385
|
+
const fallingWith = new Map<string, string[]>();
|
|
386
|
+
for (const { id, heldByTests } of swept.sweep.candidates) {
|
|
332
387
|
if (heldByTests !== undefined) {
|
|
333
|
-
fallingWith.set(heldByTests, (fallingWith.get(heldByTests) ??
|
|
388
|
+
fallingWith.set(heldByTests, [...(fallingWith.get(heldByTests) ?? []), id]);
|
|
334
389
|
}
|
|
335
390
|
}
|
|
391
|
+
const listed = membersListed(swept);
|
|
336
392
|
|
|
337
393
|
const done = findings.map((finding): CompletedFinding => {
|
|
338
394
|
const row = kinds.get(finding.code);
|
|
@@ -383,19 +439,50 @@ export function completed(
|
|
|
383
439
|
let component = found;
|
|
384
440
|
if (component === undefined) {
|
|
385
441
|
minted += 1;
|
|
442
|
+
const falling = symbol?.kind === "interface" ? fallingWith.get(symbol.id) : undefined;
|
|
386
443
|
component = {
|
|
387
444
|
id: componentId(minted),
|
|
388
445
|
root: true,
|
|
389
|
-
symbolCount:
|
|
446
|
+
symbolCount: falling?.length ?? 1,
|
|
390
447
|
deletableLines: 0,
|
|
448
|
+
...(input.config.reporters.cascade !== "full"
|
|
449
|
+
? {}
|
|
450
|
+
: falling === undefined
|
|
451
|
+
? {
|
|
452
|
+
members: [
|
|
453
|
+
{
|
|
454
|
+
ref: finding.symbol.ref,
|
|
455
|
+
name: finding.symbol.name,
|
|
456
|
+
position: finding.position,
|
|
457
|
+
},
|
|
458
|
+
],
|
|
459
|
+
}
|
|
460
|
+
: { members: listed(falling) }),
|
|
391
461
|
};
|
|
392
462
|
}
|
|
393
463
|
const counts =
|
|
394
464
|
candidate === undefined
|
|
395
465
|
? union.made[at]
|
|
396
466
|
: { production: candidate.productionRefs, test: candidate.testRefs };
|
|
397
|
-
const
|
|
467
|
+
const ownClass =
|
|
398
468
|
judged === undefined ? (finding.reachabilityClass ?? "certain") : classOf(judged.id);
|
|
469
|
+
const used =
|
|
470
|
+
judged !== undefined
|
|
471
|
+
? candidate !== undefined && testUsed.declarations.has(judged.id)
|
|
472
|
+
: finding.code === FILE_NEVER_IMPORTED && testUsed.files.has(finding.position.path);
|
|
473
|
+
const reachabilityClass = used && candidate?.testOfDeadCode !== true ? "possible" : ownClass;
|
|
474
|
+
const configurations =
|
|
475
|
+
candidate?.configurations ??
|
|
476
|
+
(judged === undefined && !part
|
|
477
|
+
? swept.matrix.configurations
|
|
478
|
+
: (swept.matrix.heldIn[at] ?? swept.matrix.configurations));
|
|
479
|
+
const untested = capped(
|
|
480
|
+
capped(
|
|
481
|
+
capped(ownClass, row.maxClass),
|
|
482
|
+
rootsCeiling(input, finding, candidate, configurations),
|
|
483
|
+
),
|
|
484
|
+
judged === undefined ? "certain" : ceilingOf(judged.id),
|
|
485
|
+
);
|
|
399
486
|
const generated = input.files.generated?.has(finding.position.path) === true;
|
|
400
487
|
const fixability = generated
|
|
401
488
|
? "none"
|
|
@@ -420,20 +507,14 @@ export function completed(
|
|
|
420
507
|
position: finding.position,
|
|
421
508
|
symbol: finding.symbol,
|
|
422
509
|
reachabilityClass,
|
|
423
|
-
confidence:
|
|
424
|
-
|
|
425
|
-
judged === undefined ? "certain" : ceilingOf(judged.id),
|
|
426
|
-
),
|
|
510
|
+
confidence: used ? "possible" : untested,
|
|
511
|
+
...(used ? { componentConfidence: untested } : {}),
|
|
427
512
|
...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
|
|
428
513
|
testOnly: judged !== undefined && counts?.production === 0 && counts.test > 0,
|
|
429
514
|
generated,
|
|
430
515
|
component,
|
|
431
516
|
retainedBy: [],
|
|
432
|
-
configurations
|
|
433
|
-
candidate?.configurations ??
|
|
434
|
-
(judged === undefined && !part
|
|
435
|
-
? swept.matrix.configurations
|
|
436
|
-
: (swept.matrix.heldIn[at] ?? swept.matrix.configurations)),
|
|
517
|
+
configurations,
|
|
437
518
|
consumersLoaded: input.boundary.consumers.loaded,
|
|
438
519
|
fixability,
|
|
439
520
|
severity: severityOf(input, finding.code, row.defaultSeverity),
|
|
@@ -450,7 +531,7 @@ export function completed(
|
|
|
450
531
|
/**
|
|
451
532
|
* The findings with every finding of a dead component capped by the lowest confidence
|
|
452
533
|
* among the component's root members, so the minimum confidence reports or withholds a
|
|
453
|
-
* component whole.
|
|
534
|
+
* component whole but for the findings test-used code lowers on their own.
|
|
454
535
|
*/
|
|
455
536
|
function componentCapped(
|
|
456
537
|
findings: readonly CompletedFinding[],
|
|
@@ -461,17 +542,29 @@ function componentCapped(
|
|
|
461
542
|
const { id, root } = finding.component;
|
|
462
543
|
if (root && dead.has(id)) {
|
|
463
544
|
const held = lowest.get(id);
|
|
464
|
-
|
|
545
|
+
const own = componentConfidenceOf(finding);
|
|
546
|
+
lowest.set(id, held === undefined ? own : capped(held, own));
|
|
465
547
|
}
|
|
466
548
|
}
|
|
467
549
|
return findings.map((finding) => {
|
|
468
550
|
const cap = lowest.get(finding.component.id);
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
551
|
+
if (cap === undefined) {
|
|
552
|
+
return finding;
|
|
553
|
+
}
|
|
554
|
+
const own = finding.componentConfidence;
|
|
555
|
+
return {
|
|
556
|
+
...finding,
|
|
557
|
+
confidence: capped(finding.confidence, cap),
|
|
558
|
+
...(own === undefined ? {} : { componentConfidence: capped(own, cap) }),
|
|
559
|
+
};
|
|
472
560
|
});
|
|
473
561
|
}
|
|
474
562
|
|
|
563
|
+
/** The confidence a finding gives its component's cap and the dials. */
|
|
564
|
+
function componentConfidenceOf(finding: CompletedFinding): Confidence {
|
|
565
|
+
return finding.componentConfidence ?? finding.confidence;
|
|
566
|
+
}
|
|
567
|
+
|
|
475
568
|
/** Whether the dials withhold a finding by its own severity or confidence. */
|
|
476
569
|
function dialed(
|
|
477
570
|
finding: Pick<CompletedFinding, "severity" | "confidence">,
|
|
@@ -494,7 +587,11 @@ export function dialsOf(
|
|
|
494
587
|
const minConfidence = input.config.analysis.minConfidence;
|
|
495
588
|
const components = new Set(
|
|
496
589
|
findings
|
|
497
|
-
.filter(
|
|
590
|
+
.filter(
|
|
591
|
+
(finding) =>
|
|
592
|
+
finding.component.root &&
|
|
593
|
+
dialed({ ...finding, confidence: componentConfidenceOf(finding) }, minConfidence),
|
|
594
|
+
)
|
|
498
595
|
.map((finding) => finding.component.id),
|
|
499
596
|
);
|
|
500
597
|
return {
|
|
@@ -42,7 +42,8 @@ type ParameterVerdict =
|
|
|
42
42
|
* caller may pass a function used as a value more arguments than it declares, so there one
|
|
43
43
|
* a read parameter follows is renamed to an underscore name, which the compilers read as
|
|
44
44
|
* declared unread, and one no read parameter follows is deleted: kept where the call's
|
|
45
|
-
* inference may read its annotation, at `probable` where nothing reads the annotation
|
|
45
|
+
* inference may read its annotation, at `probable` where nothing reads the annotation or
|
|
46
|
+
* where it is a name a contextually typed object pattern binds.
|
|
46
47
|
*/
|
|
47
48
|
function parameterVerdict(
|
|
48
49
|
part: PartFact,
|
|
@@ -63,7 +64,8 @@ function parameterVerdict(
|
|
|
63
64
|
if (place?.annotated === "steers") {
|
|
64
65
|
return undefined;
|
|
65
66
|
}
|
|
66
|
-
|
|
67
|
+
const unseen = place?.annotated === "free" || place?.contextualPattern === true;
|
|
68
|
+
return { fix: "delete", confidence: unseen ? "probable" : "certain" };
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
/**
|
|
@@ -8,8 +8,8 @@ import type { Emitter, EmitterInput } from "./emitter.ts";
|
|
|
8
8
|
/** The codes of this family. */
|
|
9
9
|
type ReadsAndWritesCode = "DS1301" | "DS1302" | "DS1303";
|
|
10
10
|
|
|
11
|
-
/** The containers whose type parameters `DS1303` reports: a function and a
|
|
12
|
-
const SIGNATURES: ReadonlySet<string> = new Set(["function", "method"]);
|
|
11
|
+
/** The containers whose type parameters `DS1303` reports: a function, a class method and a type alias. */
|
|
12
|
+
const SIGNATURES: ReadonlySet<string> = new Set(["function", "method", "type"]);
|
|
13
13
|
|
|
14
14
|
/** What the family reads of one run, indexed by declaration. */
|
|
15
15
|
interface Run {
|
|
@@ -225,10 +225,11 @@ function unusedEnumMembers(run: Run): readonly Finding[] {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
/**
|
|
228
|
-
* `DS1303`: a type parameter of a function
|
|
229
|
-
* declaration's signature nor its body names.
|
|
230
|
-
* a
|
|
231
|
-
*
|
|
228
|
+
* `DS1303`: a type parameter of a function, a class method or a type alias that neither
|
|
229
|
+
* the declaration's signature nor its body names. An alias names no type of its own, so
|
|
230
|
+
* a parameter its type never names makes no two instantiations distinct. One of a class,
|
|
231
|
+
* an interface or a method signature is never reported: a class or an interface merges
|
|
232
|
+
* only with declarations of the same parameters.
|
|
232
233
|
*/
|
|
233
234
|
function unusedTypeParameters(run: Run): readonly Finding[] {
|
|
234
235
|
return unnamed(run, "type-parameter")
|