@cplieger/deadset-ts 8.0.0-dev.2 → 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 +27 -5
- package/dist/src/augmentations.js +62 -0
- package/dist/src/build-scripts.js +437 -0
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/convention-rows.js +39 -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 +62 -26
- 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 +4 -1
- package/dist/src/graph.js +5 -3
- package/dist/src/intra-function-parts.js +45 -17
- package/dist/src/kinds.js +7 -7
- package/dist/src/manifest.js +18 -0
- package/dist/src/matrix.js +38 -4
- 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/sweep.js +7 -9
- package/dist/src/test-globals.js +234 -0
- 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 +45 -7
- package/src/augmentations.ts +76 -1
- package/src/build-scripts.ts +564 -0
- package/src/conformance-record.ts +1 -1
- package/src/convention-rows.ts +47 -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/findings/completion.ts +72 -28
- package/src/findings/intra-function.ts +4 -2
- package/src/findings/reads-and-writes.ts +7 -6
- package/src/findings/unused-declarations.ts +6 -1
- package/src/graph.ts +5 -3
- package/src/intra-function-parts.ts +55 -16
- package/src/kinds.ts +7 -7
- package/src/manifest.ts +21 -3
- package/src/matrix.ts +51 -4
- 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/sweep.ts +21 -9
- package/src/test-globals.ts +275 -0
- 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/convention-rows.ts
CHANGED
|
@@ -146,8 +146,8 @@ const SCRIPT = "{js,jsx,ts,tsx}";
|
|
|
146
146
|
const MODULE = "{js,mjs,ts,mts}";
|
|
147
147
|
const ANY_MODULE = "{js,mjs,cjs,ts,mts,cts}";
|
|
148
148
|
const VITE_CONFIG = `vite.config.${ANY_MODULE}`;
|
|
149
|
-
/** The legacy configuration files of ESLint
|
|
150
|
-
const ESLINTRC = [".eslintrc", ".eslintrc.json", ".eslintrc.{js,cjs}"];
|
|
149
|
+
/** The legacy configuration files of ESLint: JSON, YAML or a CommonJS module. */
|
|
150
|
+
const ESLINTRC = [".eslintrc", ".eslintrc.json", ".eslintrc.{yml,yaml}", ".eslintrc.{js,cjs}"];
|
|
151
151
|
|
|
152
152
|
const SVELTEKIT_ROUTE_FILES = "+{page,layout,server,page.server,layout.server,error}";
|
|
153
153
|
|
|
@@ -209,6 +209,35 @@ const ANGULAR_PACKAGE_KEYS: readonly PackageKey[] = [
|
|
|
209
209
|
"scripts",
|
|
210
210
|
].map((key) => ({ key, files: ["angular.json"] }));
|
|
211
211
|
|
|
212
|
+
/**
|
|
213
|
+
* The row of a code generator that writes its output below one directory its
|
|
214
|
+
* configuration's `property` sets, `fallback` where no configuration file sets it, so
|
|
215
|
+
* every file there is a generated file.
|
|
216
|
+
*/
|
|
217
|
+
function outputDirectoryRow(row: {
|
|
218
|
+
readonly name: string;
|
|
219
|
+
readonly package: string;
|
|
220
|
+
readonly range: string;
|
|
221
|
+
readonly files: readonly string[];
|
|
222
|
+
readonly property: string;
|
|
223
|
+
readonly fallback: string;
|
|
224
|
+
}): ConventionRow {
|
|
225
|
+
return {
|
|
226
|
+
name: row.name,
|
|
227
|
+
package: row.package,
|
|
228
|
+
range: row.range,
|
|
229
|
+
entries: [],
|
|
230
|
+
generated: ["<output>"],
|
|
231
|
+
moves: [
|
|
232
|
+
{
|
|
233
|
+
id: "output",
|
|
234
|
+
defaults: [row.fallback],
|
|
235
|
+
readings: [{ property: row.property, files: row.files }],
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
212
241
|
/**
|
|
213
242
|
* The test runners and the assertion, mock and runner libraries whose import is test
|
|
214
243
|
* evidence beside each runner a row reads test files for. Each applies whether or not a
|
|
@@ -299,6 +328,7 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
|
|
|
299
328
|
range: ">=4.0.0 <10.0.0",
|
|
300
329
|
entries: [],
|
|
301
330
|
moves: [],
|
|
331
|
+
packageKeys: [{ key: "parser", files: ESLINTRC }],
|
|
302
332
|
prefixedNames: [
|
|
303
333
|
{ property: "plugins", files: ESLINTRC, prefix: "eslint-plugin" },
|
|
304
334
|
{ property: "extends", files: ESLINTRC, prefix: "eslint-plugin", marker: "plugin:" },
|
|
@@ -414,6 +444,14 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
|
|
|
414
444
|
{ key: "oclif.hooks", directory: false },
|
|
415
445
|
],
|
|
416
446
|
},
|
|
447
|
+
outputDirectoryRow({
|
|
448
|
+
name: "panda",
|
|
449
|
+
package: "@pandacss/dev",
|
|
450
|
+
range: ">=0.1.0 <2.0.0",
|
|
451
|
+
files: [`panda.config.${ANY_MODULE}`],
|
|
452
|
+
property: "outdir",
|
|
453
|
+
fallback: "styled-system",
|
|
454
|
+
}),
|
|
417
455
|
{
|
|
418
456
|
name: "parcel",
|
|
419
457
|
package: "parcel",
|
|
@@ -481,6 +519,13 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
|
|
|
481
519
|
},
|
|
482
520
|
],
|
|
483
521
|
},
|
|
522
|
+
{
|
|
523
|
+
name: "react-scripts",
|
|
524
|
+
package: "react-scripts",
|
|
525
|
+
range: ">=1.0.0 <6.0.0",
|
|
526
|
+
entries: [`src/index.${SCRIPT}`, "src/setupProxy.js", `src/setupTests.${SCRIPT}`],
|
|
527
|
+
moves: [],
|
|
528
|
+
},
|
|
484
529
|
{
|
|
485
530
|
name: "remix",
|
|
486
531
|
package: "@remix-run/dev",
|
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. */
|
|
@@ -132,6 +132,20 @@ 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
|
+
|
|
135
149
|
/** The target symbols one test of dead code references, by id: production declarations the sweep judges. */
|
|
136
150
|
function testTargets(union: Graph, id: string): readonly string[] {
|
|
137
151
|
return (union.out[union.at(id)] ?? []).flatMap((edge) => {
|
|
@@ -197,17 +211,21 @@ function testUsedSubjects(input: EmitterInput): {
|
|
|
197
211
|
|
|
198
212
|
/**
|
|
199
213
|
* The ceiling the roots the run found put on one dead subject: a subject other declarations
|
|
200
|
-
* reference, found dead only because no root reaches them, is `probable` unless
|
|
201
|
-
*
|
|
202
|
-
*
|
|
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`.
|
|
203
218
|
*/
|
|
204
219
|
function rootsCeiling(
|
|
205
220
|
input: EmitterInput,
|
|
206
221
|
finding: Finding,
|
|
207
222
|
candidate: Candidate | undefined,
|
|
223
|
+
configurations: readonly string[],
|
|
208
224
|
): Confidence {
|
|
209
225
|
const unreachable = candidate?.relation === "reachability" || finding.unreachableOnly === true;
|
|
210
|
-
|
|
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";
|
|
211
229
|
}
|
|
212
230
|
|
|
213
231
|
/**
|
|
@@ -225,9 +243,28 @@ function findingComponents(
|
|
|
225
243
|
held.set(member, component);
|
|
226
244
|
}
|
|
227
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[] {
|
|
228
265
|
const symbols = new Map(swept.matrix.union.symbols.map((symbol) => [symbol.id, symbol]));
|
|
229
|
-
|
|
230
|
-
|
|
266
|
+
return (members) =>
|
|
267
|
+
members
|
|
231
268
|
.flatMap((member) => {
|
|
232
269
|
const symbol = symbols.get(member);
|
|
233
270
|
return symbol === undefined
|
|
@@ -244,18 +281,6 @@ function findingComponents(
|
|
|
244
281
|
];
|
|
245
282
|
})
|
|
246
283
|
.sort((a, b) => byPosition(a.position, b.position));
|
|
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) } : {}),
|
|
257
|
-
};
|
|
258
|
-
};
|
|
259
284
|
}
|
|
260
285
|
|
|
261
286
|
/**
|
|
@@ -357,12 +382,13 @@ export function completed(
|
|
|
357
382
|
// A join keeps its first component's identifier, so a minted one counts past every computed one.
|
|
358
383
|
let minted = computed;
|
|
359
384
|
const dead = new Set(components.map((component) => component.id));
|
|
360
|
-
const fallingWith = new Map<string,
|
|
361
|
-
for (const { heldByTests } of swept.sweep.candidates) {
|
|
385
|
+
const fallingWith = new Map<string, string[]>();
|
|
386
|
+
for (const { id, heldByTests } of swept.sweep.candidates) {
|
|
362
387
|
if (heldByTests !== undefined) {
|
|
363
|
-
fallingWith.set(heldByTests, (fallingWith.get(heldByTests) ??
|
|
388
|
+
fallingWith.set(heldByTests, [...(fallingWith.get(heldByTests) ?? []), id]);
|
|
364
389
|
}
|
|
365
390
|
}
|
|
391
|
+
const listed = membersListed(swept);
|
|
366
392
|
|
|
367
393
|
const done = findings.map((finding): CompletedFinding => {
|
|
368
394
|
const row = kinds.get(finding.code);
|
|
@@ -413,11 +439,25 @@ export function completed(
|
|
|
413
439
|
let component = found;
|
|
414
440
|
if (component === undefined) {
|
|
415
441
|
minted += 1;
|
|
442
|
+
const falling = symbol?.kind === "interface" ? fallingWith.get(symbol.id) : undefined;
|
|
416
443
|
component = {
|
|
417
444
|
id: componentId(minted),
|
|
418
445
|
root: true,
|
|
419
|
-
symbolCount:
|
|
446
|
+
symbolCount: falling?.length ?? 1,
|
|
420
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) }),
|
|
421
461
|
};
|
|
422
462
|
}
|
|
423
463
|
const counts =
|
|
@@ -431,8 +471,16 @@ export function completed(
|
|
|
431
471
|
? candidate !== undefined && testUsed.declarations.has(judged.id)
|
|
432
472
|
: finding.code === FILE_NEVER_IMPORTED && testUsed.files.has(finding.position.path);
|
|
433
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));
|
|
434
479
|
const untested = capped(
|
|
435
|
-
capped(
|
|
480
|
+
capped(
|
|
481
|
+
capped(ownClass, row.maxClass),
|
|
482
|
+
rootsCeiling(input, finding, candidate, configurations),
|
|
483
|
+
),
|
|
436
484
|
judged === undefined ? "certain" : ceilingOf(judged.id),
|
|
437
485
|
);
|
|
438
486
|
const generated = input.files.generated?.has(finding.position.path) === true;
|
|
@@ -466,11 +514,7 @@ export function completed(
|
|
|
466
514
|
generated,
|
|
467
515
|
component,
|
|
468
516
|
retainedBy: [],
|
|
469
|
-
configurations
|
|
470
|
-
candidate?.configurations ??
|
|
471
|
-
(judged === undefined && !part
|
|
472
|
-
? swept.matrix.configurations
|
|
473
|
-
: (swept.matrix.heldIn[at] ?? swept.matrix.configurations)),
|
|
517
|
+
configurations,
|
|
474
518
|
consumersLoaded: input.boundary.consumers.loaded,
|
|
475
519
|
fixability,
|
|
476
520
|
severity: severityOf(input, finding.code, row.defaultSeverity),
|
|
@@ -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")
|
|
@@ -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,6 +56,8 @@ 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
|
/**
|
|
@@ -68,7 +71,7 @@ interface Judged {
|
|
|
68
71
|
function codeOf(judged: Judged): string | undefined {
|
|
69
72
|
const { candidate, symbol } = judged;
|
|
70
73
|
if (candidate.testOfDeadCode) {
|
|
71
|
-
return "DS1005";
|
|
74
|
+
return judged.testsUnseenAPI ? undefined : "DS1005";
|
|
72
75
|
}
|
|
73
76
|
if ((judged.test && !candidate.unreferencedTest) || NOT_THIS_FAMILY.has(symbol.kind)) {
|
|
74
77
|
return undefined;
|
|
@@ -241,6 +244,7 @@ function familyFindings(input: EmitterInput, fallen: boolean): Finding[] {
|
|
|
241
244
|
const union = input.swept.matrix.union;
|
|
242
245
|
const dead = new Set(input.swept.sweep.candidates.map((candidate) => candidate.id));
|
|
243
246
|
const unreachable = unreachableExports(input);
|
|
247
|
+
const unseenAPI = testsUnseenAPI(input);
|
|
244
248
|
let brands: ReadonlySet<string> | undefined;
|
|
245
249
|
|
|
246
250
|
const found: Finding[] = [];
|
|
@@ -260,6 +264,7 @@ function familyFindings(input: EmitterInput, fallen: boolean): Finding[] {
|
|
|
260
264
|
test: union.test[at] === true,
|
|
261
265
|
deprecated: input.deprecated.has(symbol.id),
|
|
262
266
|
unreachable: unreachable.has(symbol.id),
|
|
267
|
+
testsUnseenAPI: candidate.testOfDeadCode && unseenAPI(candidate.id),
|
|
263
268
|
};
|
|
264
269
|
const code = codeOf(judged);
|
|
265
270
|
if (
|
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,
|