@cplieger/deadset-ts 5.2.3 → 6.0.0
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 +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/dependencies-and-module-machinery.js +2 -2
- package/dist/src/findings/emitters.js +47 -12
- package/dist/src/findings/interfaces.js +4 -3
- package/dist/src/findings/intra-function.js +15 -2
- package/dist/src/findings/unused-declarations.js +13 -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 +1 -0
- package/dist/src/references.js +6 -0
- package/dist/src/report.js +6 -1
- package/dist/src/reporters/sarif.js +3 -1
- package/dist/src/reporters/text.js +23 -3
- package/dist/src/roots.js +50 -10
- package/dist/src/version.js +1 -1
- package/dist/src/workflow-steps.js +82 -0
- package/jsr.json +1 -1
- package/package.json +7 -1
- 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/dependencies-and-module-machinery.ts +2 -2
- package/src/findings/emitters.ts +71 -12
- package/src/findings/interfaces.ts +7 -6
- package/src/findings/intra-function.ts +20 -2
- package/src/findings/unused-declarations.ts +16 -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 +1 -0
- package/src/references.ts +6 -0
- package/src/report.ts +11 -1
- package/src/reporters/sarif.ts +4 -1
- package/src/reporters/text.ts +25 -3
- package/src/roots.ts +56 -7
- package/src/sweep.ts +12 -0
- package/src/version.ts +1 -1
- package/src/workflow-steps.ts +104 -0
|
@@ -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
|
+
}
|
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.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "Deterministic dead-code analysis for TypeScript, class and type members included",
|
|
@@ -31,8 +31,14 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@typescript/native": "npm:typescript@7.1.0-dev.20261006.1"
|
|
33
33
|
},
|
|
34
|
+
"overrides": {
|
|
35
|
+
"qs": "^6.15.2"
|
|
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
|
];
|
package/src/conventions.ts
CHANGED
|
@@ -50,9 +50,11 @@ import {
|
|
|
50
50
|
CONVENTION_ROWS,
|
|
51
51
|
type ConventionRow,
|
|
52
52
|
type DirectoryMove,
|
|
53
|
+
type MoveReading,
|
|
53
54
|
type OptionsCall,
|
|
54
55
|
type ShortNameReading,
|
|
55
56
|
} from "./convention-rows.ts";
|
|
57
|
+
import { documentStrings } from "./configuration-files.ts";
|
|
56
58
|
import { globExpression } from "./glob.ts";
|
|
57
59
|
import type { Host } from "./host.ts";
|
|
58
60
|
import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from "./paths.ts";
|
|
@@ -108,8 +110,18 @@ export interface Conventions {
|
|
|
108
110
|
readonly globs: readonly ConventionGlob[];
|
|
109
111
|
readonly generated: readonly GeneratedDirectory[];
|
|
110
112
|
readonly failures: readonly ConventionFailure[];
|
|
111
|
-
/** The dependencies the applied rows' short names use, by name, each once. */
|
|
113
|
+
/** The dependencies the applied rows' short names and manifest keys use, by name, each once. */
|
|
112
114
|
readonly uses: readonly string[];
|
|
115
|
+
/** Per configuration file, absolute, the strings its applied rows' selection keys hold. */
|
|
116
|
+
readonly selected: ReadonlyMap<string, Selected>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The strings one configuration file holds at selection keys, which root no file. */
|
|
120
|
+
export interface Selected {
|
|
121
|
+
/** Where each string starts in the file's text. */
|
|
122
|
+
readonly starts: ReadonlySet<number>;
|
|
123
|
+
/** Each string's text, once per occurrence. */
|
|
124
|
+
readonly texts: readonly string[];
|
|
113
125
|
}
|
|
114
126
|
|
|
115
127
|
/** One file a row roots. */
|
|
@@ -520,6 +532,86 @@ function readPath(
|
|
|
520
532
|
return undefined;
|
|
521
533
|
}
|
|
522
534
|
|
|
535
|
+
/**
|
|
536
|
+
* Every string literal one file writes at one property's path inside the objects it is
|
|
537
|
+
* read from, as {@link readProperty} reads them: the value itself, or each element of an
|
|
538
|
+
* array value, whatever else the path holds.
|
|
539
|
+
*/
|
|
540
|
+
function stringsAt(file: SourceFile, property: string, call?: OptionsCall): readonly Node[] {
|
|
541
|
+
const declared = declaredValues(file);
|
|
542
|
+
const objects =
|
|
543
|
+
call === undefined
|
|
544
|
+
? defaultExports(file).map((one) => objectOf(one, declared, true))
|
|
545
|
+
: optionsArguments(file, call).map((one) =>
|
|
546
|
+
one === undefined ? undefined : objectOf(one, declared, false),
|
|
547
|
+
);
|
|
548
|
+
const found: Node[] = [];
|
|
549
|
+
const visit = (object: Node, keys: readonly string[]): void => {
|
|
550
|
+
if (!isObjectLiteralExpression(object)) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const [key, ...rest] = keys;
|
|
554
|
+
for (const member of object.properties) {
|
|
555
|
+
if (!isPropertyAssignment(member) || keyOf(member) !== key) {
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
const held = unwrapped(member.initializer);
|
|
559
|
+
if (rest.length > 0) {
|
|
560
|
+
visit(held, rest);
|
|
561
|
+
} else if (literalText(held) !== undefined) {
|
|
562
|
+
found.push(held);
|
|
563
|
+
} else if (isArrayLiteralExpression(held)) {
|
|
564
|
+
found.push(...held.elements.map(unwrapped).filter((one) => literalText(one) !== undefined));
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
for (const object of objects) {
|
|
569
|
+
if (object !== undefined) {
|
|
570
|
+
visit(object, property.split("."));
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return found;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** The strings an applied row's selection keys hold, added per configuration file to `into`. */
|
|
577
|
+
function selectedStrings(
|
|
578
|
+
host: Host,
|
|
579
|
+
parse: ParseSource,
|
|
580
|
+
dir: string,
|
|
581
|
+
readings: readonly MoveReading[],
|
|
582
|
+
into: Map<string, { starts: Set<number>; texts: string[] }>,
|
|
583
|
+
): void {
|
|
584
|
+
for (const reading of readings) {
|
|
585
|
+
for (const { name, file } of readingFiles(host, parse, dir, reading.files)) {
|
|
586
|
+
const path = joinPath(dir, name);
|
|
587
|
+
const held = into.get(path) ?? { starts: new Set<number>(), texts: [] };
|
|
588
|
+
for (const node of stringsAt(file, reading.property, reading.call)) {
|
|
589
|
+
held.starts.add(node.getStart());
|
|
590
|
+
held.texts.push(literalText(node) ?? "");
|
|
591
|
+
}
|
|
592
|
+
into.set(path, held);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* The dependencies one manifest declares that a string at any depth of one of its members
|
|
599
|
+
* names, alone or followed by `/` and a subpath, a member's name aside.
|
|
600
|
+
*/
|
|
601
|
+
function manifestKeyUses(manifest: unknown, keys: readonly string[]): readonly string[] {
|
|
602
|
+
if (!isRecord(manifest)) {
|
|
603
|
+
return [];
|
|
604
|
+
}
|
|
605
|
+
const declared = DEPENDENCY_SECTIONS.flatMap((section) => {
|
|
606
|
+
const held = manifest[section];
|
|
607
|
+
return isRecord(held) ? Object.keys(held) : [];
|
|
608
|
+
});
|
|
609
|
+
const strings = keys.flatMap((key) => documentStrings(manifest[key]));
|
|
610
|
+
return declared.filter((name) =>
|
|
611
|
+
strings.some((text) => text === name || text.startsWith(`${name}/`)),
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
523
615
|
/** What one configuration file states about one property. */
|
|
524
616
|
interface PropertyReading {
|
|
525
617
|
/** The literal values the file sets the property to. */
|
|
@@ -740,6 +832,7 @@ export function readConventions(
|
|
|
740
832
|
const generated: GeneratedDirectory[] = [];
|
|
741
833
|
const failures: ConventionFailure[] = [];
|
|
742
834
|
const uses = new Set<string>();
|
|
835
|
+
const selected = new Map<string, { starts: Set<number>; texts: string[] }>();
|
|
743
836
|
for (const manifest of manifests) {
|
|
744
837
|
const declared = readJSON(host, joinPath(manifest.dir, "package.json"));
|
|
745
838
|
const versions = new Map<string, string | undefined>();
|
|
@@ -801,6 +894,10 @@ export function readConventions(
|
|
|
801
894
|
for (const name of shortNameUses(host, parse, manifest.dir, declared, row.shortNames ?? [])) {
|
|
802
895
|
uses.add(name);
|
|
803
896
|
}
|
|
897
|
+
for (const name of manifestKeyUses(declared, row.manifestKeys ?? [])) {
|
|
898
|
+
uses.add(name);
|
|
899
|
+
}
|
|
900
|
+
selectedStrings(host, parse, manifest.dir, row.selections ?? [], selected);
|
|
804
901
|
const excludes = [
|
|
805
902
|
...new Set((row.excludes ?? []).flatMap((one) => expand(one, resolved))),
|
|
806
903
|
].map(globExpression);
|
|
@@ -824,7 +921,14 @@ export function readConventions(
|
|
|
824
921
|
compare(a.manifest, b.manifest) ||
|
|
825
922
|
compare(JSON.stringify(a), JSON.stringify(b)),
|
|
826
923
|
);
|
|
827
|
-
return {
|
|
924
|
+
return {
|
|
925
|
+
applied: unique,
|
|
926
|
+
globs,
|
|
927
|
+
generated,
|
|
928
|
+
failures,
|
|
929
|
+
uses: [...uses].sort(compare),
|
|
930
|
+
selected,
|
|
931
|
+
};
|
|
828
932
|
}
|
|
829
933
|
|
|
830
934
|
/**
|