@cplieger/deadset-ts 5.2.3 → 6.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/src/analysis.js +28 -5
- package/dist/src/augmentations.js +50 -0
- package/dist/src/components.js +15 -15
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/convention-rows.js +17 -2
- package/dist/src/conventions.js +82 -1
- package/dist/src/dependencies.js +42 -26
- package/dist/src/entry-points.js +28 -14
- package/dist/src/exempt.js +1 -0
- package/dist/src/findings/completion.js +9 -4
- package/dist/src/findings/dependencies-and-module-machinery.js +2 -2
- package/dist/src/findings/emitters.js +64 -25
- package/dist/src/findings/interfaces.js +4 -3
- package/dist/src/findings/intra-function.js +15 -2
- package/dist/src/findings/reads-and-writes.js +3 -2
- package/dist/src/findings/unused-declarations.js +13 -1
- package/dist/src/findings/visibility-narrowing.js +2 -1
- package/dist/src/findings-pass.js +1 -0
- package/dist/src/graph.js +6 -6
- package/dist/src/implementations.js +1 -1
- package/dist/src/indexed-access.js +215 -0
- package/dist/src/interface-satisfaction.js +85 -23
- package/dist/src/intra-function-parts.js +139 -30
- package/dist/src/inventory.js +6 -1
- package/dist/src/kinds.js +13 -12
- package/dist/src/matrix.js +34 -9
- package/dist/src/module-declarations.js +180 -0
- package/dist/src/outside-tests.js +17 -15
- package/dist/src/overrides.js +70 -9
- package/dist/src/query.js +7 -0
- package/dist/src/ref.js +9 -0
- package/dist/src/references.js +6 -0
- package/dist/src/report.js +6 -1
- package/dist/src/reporters/sarif.js +6 -2
- package/dist/src/reporters/text.js +23 -3
- package/dist/src/roots.js +50 -10
- package/dist/src/suppress-file.js +3 -5
- package/dist/src/version.js +1 -1
- package/dist/src/workflow-steps.js +82 -0
- package/dist/src/workspace-programs.js +6 -31
- package/jsr.json +1 -1
- package/package.json +8 -2
- package/src/analysis.ts +40 -4
- package/src/augmentations.ts +72 -0
- package/src/components.ts +15 -13
- package/src/conformance-record.ts +2 -2
- package/src/convention-rows.ts +25 -3
- package/src/conventions.ts +106 -2
- package/src/dependencies.ts +51 -31
- package/src/entry-points.ts +32 -19
- package/src/exempt.ts +4 -1
- package/src/findings/completion.ts +11 -4
- package/src/findings/dependencies-and-module-machinery.ts +2 -2
- package/src/findings/emitters.ts +89 -27
- package/src/findings/interfaces.ts +7 -6
- package/src/findings/intra-function.ts +20 -2
- package/src/findings/reads-and-writes.ts +7 -2
- package/src/findings/unused-declarations.ts +16 -1
- package/src/findings/visibility-narrowing.ts +2 -1
- package/src/findings-pass.ts +4 -1
- package/src/graph.ts +6 -6
- package/src/implementations.ts +1 -1
- package/src/indexed-access.ts +295 -0
- package/src/interface-satisfaction.ts +118 -27
- package/src/intra-function-parts.ts +196 -35
- package/src/inventory.ts +8 -0
- package/src/kinds.ts +14 -12
- package/src/matrix.ts +51 -8
- package/src/module-declarations.ts +241 -0
- package/src/outside-tests.ts +18 -16
- package/src/overrides.ts +80 -14
- package/src/query.ts +28 -0
- package/src/ref.ts +10 -0
- package/src/references.ts +6 -0
- package/src/report.ts +11 -1
- package/src/reporters/sarif.ts +9 -2
- package/src/reporters/text.ts +25 -3
- package/src/roots.ts +56 -7
- package/src/suppress-file.ts +3 -6
- package/src/sweep.ts +12 -0
- package/src/version.ts +1 -1
- package/src/workflow-steps.ts +104 -0
- package/src/workspace-programs.ts +6 -35
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { jsonChunks } from "./json-chunks.js";
|
|
8
8
|
import { joinPath } from "./paths.js";
|
|
9
9
|
import { positionAt, walkDocument } from "./json-document.js";
|
|
10
|
+
import { declarationKey } from "./ref.js";
|
|
10
11
|
import { CODE_FORM, SUPPRESSION_WITHOUT_REASON, SuppressionError, UNSCOPED_ENTRY, blankReason, isEntryPath, isSymbolRef, siteText, } from "./suppress.js";
|
|
11
12
|
/** The name and the location the grammar fixes for the ignore file. */
|
|
12
13
|
export const IGNORE_FILE = "deadset-ignore.json";
|
|
@@ -38,9 +39,6 @@ function documentSite(shape) {
|
|
|
38
39
|
function isObject(value) {
|
|
39
40
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
40
41
|
}
|
|
41
|
-
function referenceKey(ref, path) {
|
|
42
|
-
return `${ref}\u0000${path}`;
|
|
43
|
-
}
|
|
44
42
|
/**
|
|
45
43
|
* Reads one entry or row: the records it binds, the refusals it carries, or the
|
|
46
44
|
* malformed value that ends the run.
|
|
@@ -100,7 +98,7 @@ function readRecord(value, site, byReference, shape) {
|
|
|
100
98
|
if (refusals.length > 0) {
|
|
101
99
|
return { records: [], refusals };
|
|
102
100
|
}
|
|
103
|
-
const named = byReference.get(
|
|
101
|
+
const named = byReference.get(declarationKey(symbol, written.path)) ?? [];
|
|
104
102
|
if (named.length === 0) {
|
|
105
103
|
return { records: [{ ...written, bound: "" }], refusals: [] };
|
|
106
104
|
}
|
|
@@ -109,7 +107,7 @@ function readRecord(value, site, byReference, shape) {
|
|
|
109
107
|
function byReferenceOf(symbols) {
|
|
110
108
|
const byReference = new Map();
|
|
111
109
|
for (const symbol of symbols) {
|
|
112
|
-
const key =
|
|
110
|
+
const key = declarationKey(symbol.ref, symbol.position.path);
|
|
113
111
|
byReference.set(key, [...(byReference.get(key) ?? []), symbol]);
|
|
114
112
|
}
|
|
115
113
|
return byReference;
|
package/dist/src/version.js
CHANGED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The source files a CI workflow's `run:` steps name by their path from the target root.
|
|
3
|
+
* A workflow file is read as text, line by line, with no YAML parsed.
|
|
4
|
+
*/
|
|
5
|
+
import { scriptTokens } from "./manifest.js";
|
|
6
|
+
import { joinPath, resolvePath } from "./paths.js";
|
|
7
|
+
const WORKFLOWS = ".github/workflows";
|
|
8
|
+
/** A line holding a run command: indentation and one sequence marker before `run:`. */
|
|
9
|
+
const RUN_LINE = /^(\s*(?:- )?)run:(.*)$/u;
|
|
10
|
+
const BLOCK_SCALAR = /^[|>][+-]?$/u;
|
|
11
|
+
const QUOTED = /^(["'])(.*)\1$/su;
|
|
12
|
+
/** A token holding one of these is computed or a pattern, so it names no file. */
|
|
13
|
+
const COMPUTED = /[${*?[]/u;
|
|
14
|
+
const INDENT = /^\s*/u;
|
|
15
|
+
function workflowFiles(host, dir) {
|
|
16
|
+
let entries;
|
|
17
|
+
try {
|
|
18
|
+
entries = host.readDirectory(dir);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
return entries
|
|
24
|
+
.filter((entry) => !entry.directory && /\.ya?ml$/u.test(entry.name))
|
|
25
|
+
.map((entry) => joinPath(dir, entry.name))
|
|
26
|
+
.sort();
|
|
27
|
+
}
|
|
28
|
+
/** Every command line one workflow file's `run:` steps hold. */
|
|
29
|
+
export function runCommands(text) {
|
|
30
|
+
const lines = text.split(/\r?\n/u);
|
|
31
|
+
const found = [];
|
|
32
|
+
for (let at = 0; at < lines.length; at += 1) {
|
|
33
|
+
const match = RUN_LINE.exec(lines[at] ?? "");
|
|
34
|
+
if (match === null) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const column = (match[1] ?? "").length;
|
|
38
|
+
const value = (match[2] ?? "").trim().replace(QUOTED, "$2");
|
|
39
|
+
if (!BLOCK_SCALAR.test(value)) {
|
|
40
|
+
found.push(value);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
for (let next = at + 1; next < lines.length; next += 1) {
|
|
44
|
+
const line = lines[next] ?? "";
|
|
45
|
+
if (line.trim() !== "" && (INDENT.exec(line)?.[0].length ?? 0) <= column) {
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
found.push(line);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return found;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The own files the `run:` steps of every workflow file in the target root's
|
|
55
|
+
* `.github/workflows` name: a token equal to a file's path below the target root, with or
|
|
56
|
+
* without a leading `./`, whatever directory the step runs in.
|
|
57
|
+
*/
|
|
58
|
+
export function workflowEntries(host, targetRoot, files) {
|
|
59
|
+
const dir = joinPath(resolvePath(host.workingDirectory(), targetRoot), WORKFLOWS);
|
|
60
|
+
const found = [];
|
|
61
|
+
for (const path of workflowFiles(host, dir)) {
|
|
62
|
+
let text;
|
|
63
|
+
try {
|
|
64
|
+
text = host.readFile(path);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
for (const command of runCommands(text)) {
|
|
70
|
+
for (const token of scriptTokens(command)) {
|
|
71
|
+
if (COMPUTED.test(token)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const file = files.byPath.get(token.startsWith("./") ? token.slice(2) : token);
|
|
75
|
+
if (file !== undefined) {
|
|
76
|
+
found.push({ file, source: token });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return found;
|
|
82
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { ModuleKind, ModuleResolutionKind } from "@typescript/native/unstable/sync";
|
|
9
9
|
import { isStringLiteralLikeNode } from "@typescript/native/unstable/ast";
|
|
10
|
-
import { dirnamePath,
|
|
10
|
+
import { dirnamePath, resolvePath } from "./paths.js";
|
|
11
11
|
/** The most questions one round trip carries. */
|
|
12
12
|
const BATCH = 2048;
|
|
13
13
|
/**
|
|
@@ -20,31 +20,6 @@ function analysisOptions(parsed) {
|
|
|
20
20
|
const { composite, rootDir: _rootDir, outDir: _outDir, declarationDir: _declarationDir, tsBuildInfoFile: _tsBuildInfoFile, ...kept } = parsed.options;
|
|
21
21
|
return composite === true ? { ...kept, declaration: true } : kept;
|
|
22
22
|
}
|
|
23
|
-
/**
|
|
24
|
-
* The references a built program can carry: only where every referenced
|
|
25
|
-
* configuration reads and is composite with output, because the compiler reports a
|
|
26
|
-
* broken reference at syntax a built program does not have.
|
|
27
|
-
*/
|
|
28
|
-
function carriedReferences(engine, parsed) {
|
|
29
|
-
const references = parsed.projectReferences ?? [];
|
|
30
|
-
for (const reference of references) {
|
|
31
|
-
// A reference that names a directory names the `tsconfig.json` in it.
|
|
32
|
-
const configFile = reference.path.endsWith(".json")
|
|
33
|
-
? reference.path
|
|
34
|
-
: joinPath(reference.path, "tsconfig.json");
|
|
35
|
-
let options;
|
|
36
|
-
try {
|
|
37
|
-
options = engine.parseConfigFile(configFile).options;
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
return [];
|
|
41
|
-
}
|
|
42
|
-
if (options.composite !== true || options.noEmit === true) {
|
|
43
|
-
return [];
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return references;
|
|
47
|
-
}
|
|
48
23
|
/** The conditions an `exports` lookup under one configuration and one resolution mode reads. */
|
|
49
24
|
function conditionsOf(parsed, mode) {
|
|
50
25
|
const resolution = parsed.options.moduleResolution;
|
|
@@ -94,7 +69,7 @@ function configurationOf(engine, host, reading) {
|
|
|
94
69
|
if (reason !== undefined) {
|
|
95
70
|
return { kind: "unread", reason };
|
|
96
71
|
}
|
|
97
|
-
const { options } = reading.parsed;
|
|
72
|
+
const { options, projectReferences: references = [] } = reading.parsed;
|
|
98
73
|
const raw = options["pathsBasePath"];
|
|
99
74
|
const base = typeof raw === "string" ? raw : dirnamePath(reading.configFile);
|
|
100
75
|
const paths = {};
|
|
@@ -117,8 +92,8 @@ function configurationOf(engine, host, reading) {
|
|
|
117
92
|
...(options.composite === true ? { composite: false, declaration: true } : {}),
|
|
118
93
|
paths,
|
|
119
94
|
},
|
|
120
|
-
...(
|
|
121
|
-
? { references:
|
|
95
|
+
...(references.length > 0
|
|
96
|
+
? { references: references.map((reference) => ({ path: reference.path })) }
|
|
122
97
|
: {}),
|
|
123
98
|
...(exclude === undefined ? {} : { exclude }),
|
|
124
99
|
});
|
|
@@ -150,6 +125,7 @@ function configurationOf(engine, host, reading) {
|
|
|
150
125
|
/** The program one configuration is built as, under the answers it holds so far. */
|
|
151
126
|
function programOf(engine, reading) {
|
|
152
127
|
const compilerOptions = analysisOptions(reading.parsed);
|
|
128
|
+
const references = reading.parsed.projectReferences ?? [];
|
|
153
129
|
const resolver = engine.createModuleResolver(compilerOptions, reading.entries);
|
|
154
130
|
return {
|
|
155
131
|
resolver,
|
|
@@ -159,7 +135,7 @@ function programOf(engine, reading) {
|
|
|
159
135
|
options: {
|
|
160
136
|
moduleResolver: resolver,
|
|
161
137
|
configFileParsingDiagnostics: reading.parsed.errors,
|
|
162
|
-
...(
|
|
138
|
+
...(references.length > 0 ? { projectReferences: references } : {}),
|
|
163
139
|
},
|
|
164
140
|
},
|
|
165
141
|
};
|
|
@@ -292,7 +268,6 @@ export function workspacePrograms(engine, host, configFiles, resolver) {
|
|
|
292
268
|
{
|
|
293
269
|
configFile,
|
|
294
270
|
parsed,
|
|
295
|
-
references: carriedReferences(engine, parsed),
|
|
296
271
|
entries: [],
|
|
297
272
|
seen: new Set(),
|
|
298
273
|
bound: new Map(),
|
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/deadset-ts",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "Deterministic dead-code analysis for TypeScript, class and type members included",
|
|
@@ -29,10 +29,16 @@
|
|
|
29
29
|
"lint:fix": "eslint . --fix && prettier --write ."
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@typescript/native": "npm:typescript@7.1.0-dev.
|
|
32
|
+
"@typescript/native": "npm:typescript@7.1.0-dev.20261007.1"
|
|
33
|
+
},
|
|
34
|
+
"overrides": {
|
|
35
|
+
"qs": "^6.15.2"
|
|
33
36
|
},
|
|
34
37
|
"devDependencies": {
|
|
35
38
|
"@eslint/js": "10.0.1",
|
|
39
|
+
"@stryker-mutator/core": "10.0.0",
|
|
40
|
+
"@stryker-mutator/typescript-checker": "10.0.0",
|
|
41
|
+
"@stryker-mutator/vitest-runner": "10.0.0",
|
|
36
42
|
"@types/node": "24.19.1",
|
|
37
43
|
"eslint": "10.12.0",
|
|
38
44
|
"fast-check": "4.10.2",
|
package/src/analysis.ts
CHANGED
|
@@ -71,7 +71,7 @@ import { frameworkLifecycle } from "./framework-lifecycle.ts";
|
|
|
71
71
|
import type { Host } from "./host.ts";
|
|
72
72
|
import { implementations, mergeImplementations } from "./implementations.ts";
|
|
73
73
|
import { injectionContainer } from "./injection-container.ts";
|
|
74
|
-
import { interfaceSatisfaction } from "./interface-satisfaction.ts";
|
|
74
|
+
import { interfaceSatisfaction, settledSatisfaction } from "./interface-satisfaction.ts";
|
|
75
75
|
import { intraFunctionFacts, projectParts, type ProjectParts } from "./intra-function-parts.ts";
|
|
76
76
|
import { inventory, type Inventory, type InventorySymbol } from "./inventory.ts";
|
|
77
77
|
import { packageEntries, readManifest } from "./manifest.ts";
|
|
@@ -224,6 +224,8 @@ interface ReadProjects<Answer> {
|
|
|
224
224
|
readonly conventionsApplied: readonly AppliedConvention[];
|
|
225
225
|
/** The dependencies the applied rows' short names use. */
|
|
226
226
|
readonly conventionUses: readonly string[];
|
|
227
|
+
/** The directory of every workspace member, absolute. */
|
|
228
|
+
readonly memberDirs: readonly string[];
|
|
227
229
|
/** The test files below the target root no project the run reads holds, parsed. */
|
|
228
230
|
readonly outsideTests: readonly OutsideTest[];
|
|
229
231
|
}
|
|
@@ -508,6 +510,7 @@ function readProjects<Answer>(
|
|
|
508
510
|
componentsUnread: workspace?.componentsUnread ?? [],
|
|
509
511
|
conventionsApplied: conventions.applied,
|
|
510
512
|
conventionUses: conventions.uses,
|
|
513
|
+
memberDirs: workspace?.memberDirs ?? [],
|
|
511
514
|
outsideTests,
|
|
512
515
|
};
|
|
513
516
|
}
|
|
@@ -694,6 +697,8 @@ interface ReadRun<Extra> {
|
|
|
694
697
|
readonly conventionsApplied: readonly AppliedConvention[];
|
|
695
698
|
/** The dependencies the applied rows' short names use. */
|
|
696
699
|
readonly conventionUses: readonly string[];
|
|
700
|
+
/** The directory of every workspace member, absolute. */
|
|
701
|
+
readonly memberDirs: readonly string[];
|
|
697
702
|
}
|
|
698
703
|
|
|
699
704
|
/**
|
|
@@ -777,15 +782,20 @@ function readRun<Extra>(
|
|
|
777
782
|
withOutside.flatMap((one) => one.projectRead.rooted.liveUnderReachability),
|
|
778
783
|
withOutside.flatMap((one) => one.testFiles),
|
|
779
784
|
);
|
|
780
|
-
const
|
|
785
|
+
const settled = settledSatisfaction(
|
|
786
|
+
withOutside.map((one) => ({ evidence: one.evidence, held: one.projectRead.held })),
|
|
787
|
+
[...withOutside.flatMap((one) => one.references), ...read.consumers.references],
|
|
788
|
+
);
|
|
789
|
+
const answers = withOutside.map((one, index): SweptProject<Extra> => {
|
|
781
790
|
const { projectRead } = one;
|
|
782
|
-
const
|
|
791
|
+
const satisfied = settled[index];
|
|
792
|
+
const detected = heldRecords(satisfied?.evidence ?? one.evidence, {
|
|
783
793
|
disabled,
|
|
784
794
|
mode,
|
|
785
795
|
testFiles: new Set([...one.testFiles, ...support]),
|
|
786
796
|
});
|
|
787
797
|
const exempt = detected.records;
|
|
788
|
-
const made = supportReferences(one.references, support);
|
|
798
|
+
const made = supportReferences([...one.references, ...(satisfied?.references ?? [])], support);
|
|
789
799
|
const held = new Set(projectRead.held.symbols.map((symbol) => symbol.position.path));
|
|
790
800
|
const supportFiles = [...support].filter((path) => held.has(path)).sort();
|
|
791
801
|
const configured: Configured = {
|
|
@@ -846,6 +856,7 @@ function readRun<Extra>(
|
|
|
846
856
|
componentsUnread: read.componentsUnread,
|
|
847
857
|
conventionsApplied: read.conventionsApplied,
|
|
848
858
|
conventionUses: read.conventionUses,
|
|
859
|
+
memberDirs: read.memberDirs,
|
|
849
860
|
};
|
|
850
861
|
}
|
|
851
862
|
|
|
@@ -1070,6 +1081,30 @@ function readEmitterRun(
|
|
|
1070
1081
|
: { ...run, unansweredHeld: [...new Set([...run.unansweredHeld, ...unread])] };
|
|
1071
1082
|
}
|
|
1072
1083
|
|
|
1084
|
+
/**
|
|
1085
|
+
* The workspace members whose manifest the run reads whole enough to judge its
|
|
1086
|
+
* dependencies: a built project holds a file below the member's directory, and no derived
|
|
1087
|
+
* configuration at or below it was dropped. A file no built project holds may import any
|
|
1088
|
+
* dependency the member declares.
|
|
1089
|
+
*/
|
|
1090
|
+
function membersWhollyRead(
|
|
1091
|
+
host: Host,
|
|
1092
|
+
targetRoot: string,
|
|
1093
|
+
read: ReadRun<EmitterExtra>,
|
|
1094
|
+
): readonly string[] {
|
|
1095
|
+
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
1096
|
+
const held = new Set(read.matrix.union.symbols.map((symbol) => symbol.position.path));
|
|
1097
|
+
const dropped = read.notBuilt.map((one) => relativePath(root, one.configFile) ?? "");
|
|
1098
|
+
return read.memberDirs.filter((dir) => {
|
|
1099
|
+
const below = relativePath(root, dir);
|
|
1100
|
+
if (below === undefined) {
|
|
1101
|
+
return false;
|
|
1102
|
+
}
|
|
1103
|
+
const inside = (path: string): boolean => path.startsWith(`${below}/`);
|
|
1104
|
+
return [...held].some(inside) && !dropped.some(inside);
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1073
1108
|
/**
|
|
1074
1109
|
* The input every emitter reads over one sweep of a read run: the facts beside the sweep
|
|
1075
1110
|
* each kind family reads, every per-project one read in the same session as the sweep.
|
|
@@ -1102,6 +1137,7 @@ function emitterInputOver(
|
|
|
1102
1137
|
targetRoot,
|
|
1103
1138
|
[...projects.map((one) => one.extra.needs), configurationNeeds(read.conventionUses)],
|
|
1104
1139
|
swept.sweep.candidates.map((candidate) => candidate.id),
|
|
1140
|
+
membersWhollyRead(host, targetRoot, read),
|
|
1105
1141
|
),
|
|
1106
1142
|
implementations: mergeImplementations(projects.map((one) => one.extra.implementations)),
|
|
1107
1143
|
files: {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The references a use of an augmentation's declaration makes to what holds it. No name
|
|
3
|
+
* spells a `declare global` block, so a use of a declaration it holds is a use of the
|
|
4
|
+
* block. An interface or namespace an augmentation block holds merges with the one of its
|
|
5
|
+
* name, which the language or a library may type a value by where the target never names
|
|
6
|
+
* it, as it types `import.meta` or `process.env`, so a use of its member is a use of it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Inventory, InventorySymbol } from "./inventory.ts";
|
|
10
|
+
import type { Reference } from "./references.ts";
|
|
11
|
+
|
|
12
|
+
/** Whether one declaration is a `declare global` block: a module-level namespace named `global`. */
|
|
13
|
+
function isGlobalBlock(
|
|
14
|
+
symbol: InventorySymbol,
|
|
15
|
+
held: Inventory,
|
|
16
|
+
files: ReadonlySet<string>,
|
|
17
|
+
): boolean {
|
|
18
|
+
return (
|
|
19
|
+
symbol.kind === "namespace" &&
|
|
20
|
+
files.has(symbol.parent) &&
|
|
21
|
+
symbol.ref.endsWith("#global") &&
|
|
22
|
+
!held.modules.has(symbol.id)
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* For each reference to a declaration an augmentation block holds, a read at the same place
|
|
28
|
+
* of every interface and namespace between it and the block, and of a `declare global` block
|
|
29
|
+
* itself.
|
|
30
|
+
*/
|
|
31
|
+
export function augmentationReferences(
|
|
32
|
+
held: Inventory,
|
|
33
|
+
found: readonly Reference[],
|
|
34
|
+
): readonly Reference[] {
|
|
35
|
+
const files = new Set(held.symbols.filter((one) => one.kind === "file").map((one) => one.id));
|
|
36
|
+
const globals = new Set(
|
|
37
|
+
held.symbols.filter((one) => isGlobalBlock(one, held, files)).map((one) => one.id),
|
|
38
|
+
);
|
|
39
|
+
if (globals.size === 0 && held.modules.size === 0) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
const byId = new Map(held.symbols.map((one) => [one.id, one]));
|
|
43
|
+
const holders = (id: string): string[] => {
|
|
44
|
+
const ids: string[] = [];
|
|
45
|
+
for (
|
|
46
|
+
let at = byId.get(byId.get(id)?.parent ?? "");
|
|
47
|
+
at !== undefined;
|
|
48
|
+
at = byId.get(at.parent)
|
|
49
|
+
) {
|
|
50
|
+
if (globals.has(at.id)) {
|
|
51
|
+
return [...ids, at.id];
|
|
52
|
+
}
|
|
53
|
+
if (held.modules.has(at.id)) {
|
|
54
|
+
return ids;
|
|
55
|
+
}
|
|
56
|
+
if (at.kind === "interface" || at.kind === "namespace") {
|
|
57
|
+
ids.push(at.id);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return [];
|
|
61
|
+
};
|
|
62
|
+
return found.flatMap((reference) =>
|
|
63
|
+
holders(reference.to).map((to) => ({
|
|
64
|
+
from: reference.from,
|
|
65
|
+
to,
|
|
66
|
+
position: reference.position,
|
|
67
|
+
use: "read" as const,
|
|
68
|
+
resolution: reference.resolution,
|
|
69
|
+
test: reference.test,
|
|
70
|
+
})),
|
|
71
|
+
);
|
|
72
|
+
}
|
package/src/components.ts
CHANGED
|
@@ -62,17 +62,18 @@ export function listing(component: Component, cascade: Cascade): Listing {
|
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
64
|
* Groups the declarations `dead` marks into components, ordered by their first member's
|
|
65
|
-
* site; `dead` and `
|
|
66
|
-
* members sit on the cycles no other cycle references. A cycle two roots
|
|
67
|
-
* dead only through both, so a component is a weakly connected part of the
|
|
68
|
-
* subgraph: no dead declaration of one references a declaration of another.
|
|
65
|
+
* site; `dead`, `testOfDeadCode` and `unreferencedTest` hold one flag per declaration of
|
|
66
|
+
* the graph. Root members sit on the cycles no other cycle references. A cycle two roots
|
|
67
|
+
* both reach is dead only through both, so a component is a weakly connected part of the
|
|
68
|
+
* dead subgraph: no dead declaration of one references a declaration of another.
|
|
69
69
|
*/
|
|
70
70
|
export function componentsOf(
|
|
71
71
|
graph: Graph,
|
|
72
72
|
dead: readonly boolean[],
|
|
73
73
|
testOfDeadCode: readonly boolean[],
|
|
74
|
+
unreferencedTest: readonly boolean[] = [],
|
|
74
75
|
): readonly Component[] {
|
|
75
|
-
const { at, adjacent } = deadSubgraph(graph, dead, testOfDeadCode);
|
|
76
|
+
const { at, adjacent } = deadSubgraph(graph, dead, testOfDeadCode, unreferencedTest);
|
|
76
77
|
if (at.length === 0) {
|
|
77
78
|
return [];
|
|
78
79
|
}
|
|
@@ -133,21 +134,22 @@ function linesSpanned(symbols: readonly (InventorySymbol | undefined)[]): number
|
|
|
133
134
|
|
|
134
135
|
/**
|
|
135
136
|
* The dead declarations by their place in the subgraph, a test file's declaration only
|
|
136
|
-
* where it is a test of dead code, and the edges between them:
|
|
137
|
-
* reference one makes to another, an
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
137
|
+
* where it is a test of dead code or nothing the tests reach, and the edges between them:
|
|
138
|
+
* each reference one makes to another, except an unreferenced test-file declaration's
|
|
139
|
+
* reference to production code, which decides nothing of its target, an edge each way
|
|
140
|
+
* between a dead member and its dead container, and an edge back from each production
|
|
141
|
+
* target of an admitted test, which puts the test in its subject's component.
|
|
141
142
|
*/
|
|
142
143
|
function deadSubgraph(
|
|
143
144
|
graph: Graph,
|
|
144
145
|
dead: readonly boolean[],
|
|
145
146
|
testOfDeadCode: readonly boolean[],
|
|
147
|
+
unreferencedTest: readonly boolean[],
|
|
146
148
|
): { readonly at: readonly number[]; readonly adjacent: readonly (readonly number[])[] } {
|
|
147
149
|
const at: number[] = [];
|
|
148
150
|
const position = graph.symbols.map((_symbol, index) => {
|
|
149
|
-
|
|
150
|
-
if (dead[index] !== true || (graph.test[index] === true &&
|
|
151
|
+
const judged = testOfDeadCode[index] === true || unreferencedTest[index] === true;
|
|
152
|
+
if (dead[index] !== true || (graph.test[index] === true && !judged)) {
|
|
151
153
|
return UNVISITED;
|
|
152
154
|
}
|
|
153
155
|
at.push(index);
|
|
@@ -157,7 +159,7 @@ function deadSubgraph(
|
|
|
157
159
|
at.forEach((index, from) => {
|
|
158
160
|
for (const edge of graph.out[index] ?? []) {
|
|
159
161
|
const to = position[edge.to] ?? UNVISITED;
|
|
160
|
-
if (to === UNVISITED) {
|
|
162
|
+
if (to === UNVISITED || (unreferencedTest[index] === true && graph.test[edge.to] !== true)) {
|
|
161
163
|
continue;
|
|
162
164
|
}
|
|
163
165
|
adjacent[from]?.push(to);
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// Regenerate it with `UPDATE_GOLDEN=1 npx vitest --run src/conformance.corpus.test.ts`.
|
|
3
3
|
export const RECORD = {
|
|
4
4
|
conformance: {
|
|
5
|
-
corpusVersion: "
|
|
5
|
+
corpusVersion: "4.0.0",
|
|
6
6
|
result: "pass",
|
|
7
|
-
digest: "sha256:
|
|
7
|
+
digest: "sha256:8250b61deb71467139bf4cbcaf10802cdabb1a2c2261a11a571fc1ff55bfc3dc",
|
|
8
8
|
},
|
|
9
9
|
gaps: [],
|
|
10
10
|
} as const;
|
package/src/convention-rows.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface OptionsCall {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/** One configuration property, and the configuration files it is read from. */
|
|
19
|
-
interface MoveReading {
|
|
19
|
+
export interface MoveReading {
|
|
20
20
|
/** The property's path through nested object literals, its keys joined by `.`. */
|
|
21
21
|
readonly property: string;
|
|
22
22
|
/** Globs naming the configuration files, matched in the manifest's directory alone. */
|
|
@@ -64,6 +64,13 @@ export interface ConventionRow {
|
|
|
64
64
|
readonly moves: readonly DirectoryMove[];
|
|
65
65
|
/** The properties that name a package the configuration uses by a short name. */
|
|
66
66
|
readonly shortNames?: readonly ShortNameReading[];
|
|
67
|
+
/**
|
|
68
|
+
* The properties whose value only selects files to test, mutate, cover or ignore, so a
|
|
69
|
+
* string there names no file the tool runs or loads.
|
|
70
|
+
*/
|
|
71
|
+
readonly selections?: readonly MoveReading[];
|
|
72
|
+
/** The members of a `package.json` the tool reads its configuration from. */
|
|
73
|
+
readonly manifestKeys?: readonly string[];
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
const SCRIPT = "{js,jsx,ts,tsx}";
|
|
@@ -99,6 +106,8 @@ const SOLIDSTART_PLUGIN = { module: "@solidjs/start/config", export: "solidStart
|
|
|
99
106
|
const NUXT_CONFIG = [`nuxt.config.${ANY_MODULE}`];
|
|
100
107
|
|
|
101
108
|
const STRYKER_CONFIG = "{stryker,.stryker}.{conf,config}.{json,js,mjs,cjs}";
|
|
109
|
+
const STRYKER_MODULE = "{stryker,.stryker}.{conf,config}.{js,mjs,cjs}";
|
|
110
|
+
const VITEST_CONFIG = `{vitest,vite}.config.${ANY_MODULE}`;
|
|
102
111
|
|
|
103
112
|
/** Every row, ordered by name and then by range. */
|
|
104
113
|
export const CONVENTION_ROWS: readonly ConventionRow[] = [
|
|
@@ -214,6 +223,14 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
|
|
|
214
223
|
},
|
|
215
224
|
],
|
|
216
225
|
},
|
|
226
|
+
{
|
|
227
|
+
name: "prettier",
|
|
228
|
+
package: "prettier",
|
|
229
|
+
range: ">=3.0.0 <4.0.0",
|
|
230
|
+
entries: [`{prettier.config,.prettierrc}.${ANY_MODULE}`],
|
|
231
|
+
moves: [],
|
|
232
|
+
manifestKeys: ["prettier"],
|
|
233
|
+
},
|
|
217
234
|
{
|
|
218
235
|
name: "qwik-city",
|
|
219
236
|
package: "@builder.io/qwik-city",
|
|
@@ -331,8 +348,9 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
|
|
|
331
348
|
name: "stryker",
|
|
332
349
|
package: "@stryker-mutator/core",
|
|
333
350
|
range: ">=7.0.0 <11.0.0",
|
|
334
|
-
entries: [],
|
|
351
|
+
entries: [STRYKER_MODULE],
|
|
335
352
|
moves: [],
|
|
353
|
+
selections: [{ property: "mutate", files: [STRYKER_CONFIG] }],
|
|
336
354
|
shortNames: [
|
|
337
355
|
{
|
|
338
356
|
property: "testRunner",
|
|
@@ -476,9 +494,13 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
|
|
|
476
494
|
shortNames: [
|
|
477
495
|
{
|
|
478
496
|
property: "test.coverage.provider",
|
|
479
|
-
files: [
|
|
497
|
+
files: [VITEST_CONFIG],
|
|
480
498
|
package: "@vitest/coverage-{}",
|
|
481
499
|
},
|
|
482
500
|
],
|
|
501
|
+
selections: [
|
|
502
|
+
{ property: "test.coverage.include", files: [VITEST_CONFIG] },
|
|
503
|
+
{ property: "test.coverage.exclude", files: [VITEST_CONFIG] },
|
|
504
|
+
],
|
|
483
505
|
},
|
|
484
506
|
];
|