@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
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
deadset-ts finds dead code in TypeScript projects, down to class members, type members and dead stores inside functions, and writes a report your CI can fail on.
|
|
6
6
|
|
|
7
|
-
It reads your `tsconfig.json` projects, `allowJs` JavaScript and Vue, Svelte and Astro scripts included, with the type information of TypeScript 7.1.0-dev.
|
|
7
|
+
It reads your `tsconfig.json` projects, `allowJs` JavaScript and Vue, Svelte and Astro scripts included, with the type information of TypeScript 7.1.0-dev.20261007.1, its one dependency. Your project keeps its own TypeScript version. A type error under that version skips the function or statement that holds it. deadset-ts reports and never edits your code. It is pre-release, tested only on Linux, needs Node.js 24 or later and is licensed under GPL-3.0-or-later.
|
|
8
8
|
|
|
9
9
|
## Why use it
|
|
10
10
|
|
package/dist/src/analysis.js
CHANGED
|
@@ -27,7 +27,7 @@ import { NO_SELF_CHECK, unmatchedRootFindings, } from "./findings/self-check.js"
|
|
|
27
27
|
import { frameworkLifecycle } from "./framework-lifecycle.js";
|
|
28
28
|
import { implementations, mergeImplementations } from "./implementations.js";
|
|
29
29
|
import { injectionContainer } from "./injection-container.js";
|
|
30
|
-
import { interfaceSatisfaction } from "./interface-satisfaction.js";
|
|
30
|
+
import { interfaceSatisfaction, settledSatisfaction } from "./interface-satisfaction.js";
|
|
31
31
|
import { intraFunctionFacts, projectParts } from "./intra-function-parts.js";
|
|
32
32
|
import { inventory } from "./inventory.js";
|
|
33
33
|
import { packageEntries, readManifest } from "./manifest.js";
|
|
@@ -278,6 +278,7 @@ function readProjects(engine, host, scope, config, stage) {
|
|
|
278
278
|
componentsUnread: workspace?.componentsUnread ?? [],
|
|
279
279
|
conventionsApplied: conventions.applied,
|
|
280
280
|
conventionUses: conventions.uses,
|
|
281
|
+
memberDirs: workspace?.memberDirs ?? [],
|
|
281
282
|
outsideTests,
|
|
282
283
|
};
|
|
283
284
|
}
|
|
@@ -406,15 +407,17 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
|
|
|
406
407
|
// Test-support code is code only test code imports in any configuration of the run: a
|
|
407
408
|
// configuration that holds no test file says nothing about what the tests import.
|
|
408
409
|
const support = testSupportFiles(withOutside.flatMap((one) => one.projectRead.held.symbols), withOutside.flatMap((one) => one.references), withOutside.flatMap((one) => one.projectRead.rooted.liveUnderReachability), withOutside.flatMap((one) => one.testFiles));
|
|
409
|
-
const
|
|
410
|
+
const settled = settledSatisfaction(withOutside.map((one) => ({ evidence: one.evidence, held: one.projectRead.held })), [...withOutside.flatMap((one) => one.references), ...read.consumers.references]);
|
|
411
|
+
const answers = withOutside.map((one, index) => {
|
|
410
412
|
const { projectRead } = one;
|
|
411
|
-
const
|
|
413
|
+
const satisfied = settled[index];
|
|
414
|
+
const detected = heldRecords(satisfied?.evidence ?? one.evidence, {
|
|
412
415
|
disabled,
|
|
413
416
|
mode,
|
|
414
417
|
testFiles: new Set([...one.testFiles, ...support]),
|
|
415
418
|
});
|
|
416
419
|
const exempt = detected.records;
|
|
417
|
-
const made = supportReferences(one.references, support);
|
|
420
|
+
const made = supportReferences([...one.references, ...(satisfied?.references ?? [])], support);
|
|
418
421
|
const held = new Set(projectRead.held.symbols.map((symbol) => symbol.position.path));
|
|
419
422
|
const supportFiles = [...support].filter((path) => held.has(path)).sort();
|
|
420
423
|
const configured = {
|
|
@@ -473,6 +476,7 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
|
|
|
473
476
|
componentsUnread: read.componentsUnread,
|
|
474
477
|
conventionsApplied: read.conventionsApplied,
|
|
475
478
|
conventionUses: read.conventionUses,
|
|
479
|
+
memberDirs: read.memberDirs,
|
|
476
480
|
};
|
|
477
481
|
}
|
|
478
482
|
/**
|
|
@@ -633,6 +637,25 @@ function readEmitterRun(engine, host, scope, config, mode, detectors) {
|
|
|
633
637
|
? run
|
|
634
638
|
: { ...run, unansweredHeld: [...new Set([...run.unansweredHeld, ...unread])] };
|
|
635
639
|
}
|
|
640
|
+
/**
|
|
641
|
+
* The workspace members whose manifest the run reads whole enough to judge its
|
|
642
|
+
* dependencies: a built project holds a file below the member's directory, and no derived
|
|
643
|
+
* configuration at or below it was dropped. A file no built project holds may import any
|
|
644
|
+
* dependency the member declares.
|
|
645
|
+
*/
|
|
646
|
+
function membersWhollyRead(host, targetRoot, read) {
|
|
647
|
+
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
648
|
+
const held = new Set(read.matrix.union.symbols.map((symbol) => symbol.position.path));
|
|
649
|
+
const dropped = read.notBuilt.map((one) => relativePath(root, one.configFile) ?? "");
|
|
650
|
+
return read.memberDirs.filter((dir) => {
|
|
651
|
+
const below = relativePath(root, dir);
|
|
652
|
+
if (below === undefined) {
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
const inside = (path) => path.startsWith(`${below}/`);
|
|
656
|
+
return [...held].some(inside) && !dropped.some(inside);
|
|
657
|
+
});
|
|
658
|
+
}
|
|
636
659
|
/**
|
|
637
660
|
* The input every emitter reads over one sweep of a read run: the facts beside the sweep
|
|
638
661
|
* each kind family reads, every per-project one read in the same session as the sweep.
|
|
@@ -649,7 +672,7 @@ function emitterInputOver(host, scope, config, read, input, selfCheck) {
|
|
|
649
672
|
references: one.configured.references,
|
|
650
673
|
accessors: one.extra.accessors,
|
|
651
674
|
})), swept.exempt, input.mode),
|
|
652
|
-
dependencies: dependenciesOf(host, targetRoot, [...projects.map((one) => one.extra.needs), configurationNeeds(read.conventionUses)], swept.sweep.candidates.map((candidate) => candidate.id)),
|
|
675
|
+
dependencies: dependenciesOf(host, targetRoot, [...projects.map((one) => one.extra.needs), configurationNeeds(read.conventionUses)], swept.sweep.candidates.map((candidate) => candidate.id), membersWhollyRead(host, targetRoot, read)),
|
|
653
676
|
implementations: mergeImplementations(projects.map((one) => one.extra.implementations)),
|
|
654
677
|
files: {
|
|
655
678
|
tree: sourceTree(host, targetRoot),
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
/** Whether one declaration is a `declare global` block: a module-level namespace named `global`. */
|
|
9
|
+
function isGlobalBlock(symbol, held, files) {
|
|
10
|
+
return (symbol.kind === "namespace" &&
|
|
11
|
+
files.has(symbol.parent) &&
|
|
12
|
+
symbol.ref.endsWith("#global") &&
|
|
13
|
+
!held.modules.has(symbol.id));
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* For each reference to a declaration an augmentation block holds, a read at the same place
|
|
17
|
+
* of every interface and namespace between it and the block, and of a `declare global` block
|
|
18
|
+
* itself.
|
|
19
|
+
*/
|
|
20
|
+
export function augmentationReferences(held, found) {
|
|
21
|
+
const files = new Set(held.symbols.filter((one) => one.kind === "file").map((one) => one.id));
|
|
22
|
+
const globals = new Set(held.symbols.filter((one) => isGlobalBlock(one, held, files)).map((one) => one.id));
|
|
23
|
+
if (globals.size === 0 && held.modules.size === 0) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
const byId = new Map(held.symbols.map((one) => [one.id, one]));
|
|
27
|
+
const holders = (id) => {
|
|
28
|
+
const ids = [];
|
|
29
|
+
for (let at = byId.get(byId.get(id)?.parent ?? ""); at !== undefined; at = byId.get(at.parent)) {
|
|
30
|
+
if (globals.has(at.id)) {
|
|
31
|
+
return [...ids, at.id];
|
|
32
|
+
}
|
|
33
|
+
if (held.modules.has(at.id)) {
|
|
34
|
+
return ids;
|
|
35
|
+
}
|
|
36
|
+
if (at.kind === "interface" || at.kind === "namespace") {
|
|
37
|
+
ids.push(at.id);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return [];
|
|
41
|
+
};
|
|
42
|
+
return found.flatMap((reference) => holders(reference.to).map((to) => ({
|
|
43
|
+
from: reference.from,
|
|
44
|
+
to,
|
|
45
|
+
position: reference.position,
|
|
46
|
+
use: "read",
|
|
47
|
+
resolution: reference.resolution,
|
|
48
|
+
test: reference.test,
|
|
49
|
+
})));
|
|
50
|
+
}
|
package/dist/src/components.js
CHANGED
|
@@ -25,13 +25,13 @@ export function listing(component, cascade) {
|
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
27
|
* Groups the declarations `dead` marks into components, ordered by their first member's
|
|
28
|
-
* site; `dead` and `
|
|
29
|
-
* members sit on the cycles no other cycle references. A cycle two roots
|
|
30
|
-
* dead only through both, so a component is a weakly connected part of the
|
|
31
|
-
* subgraph: no dead declaration of one references a declaration of another.
|
|
28
|
+
* site; `dead`, `testOfDeadCode` and `unreferencedTest` hold one flag per declaration of
|
|
29
|
+
* the graph. Root members sit on the cycles no other cycle references. A cycle two roots
|
|
30
|
+
* both reach is dead only through both, so a component is a weakly connected part of the
|
|
31
|
+
* dead subgraph: no dead declaration of one references a declaration of another.
|
|
32
32
|
*/
|
|
33
|
-
export function componentsOf(graph, dead, testOfDeadCode) {
|
|
34
|
-
const { at, adjacent } = deadSubgraph(graph, dead, testOfDeadCode);
|
|
33
|
+
export function componentsOf(graph, dead, testOfDeadCode, unreferencedTest = []) {
|
|
34
|
+
const { at, adjacent } = deadSubgraph(graph, dead, testOfDeadCode, unreferencedTest);
|
|
35
35
|
if (at.length === 0) {
|
|
36
36
|
return [];
|
|
37
37
|
}
|
|
@@ -90,17 +90,17 @@ function linesSpanned(symbols) {
|
|
|
90
90
|
}
|
|
91
91
|
/**
|
|
92
92
|
* The dead declarations by their place in the subgraph, a test file's declaration only
|
|
93
|
-
* where it is a test of dead code, and the edges between them:
|
|
94
|
-
* reference one makes to another, an
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
93
|
+
* where it is a test of dead code or nothing the tests reach, and the edges between them:
|
|
94
|
+
* each reference one makes to another, except an unreferenced test-file declaration's
|
|
95
|
+
* reference to production code, which decides nothing of its target, an edge each way
|
|
96
|
+
* between a dead member and its dead container, and an edge back from each production
|
|
97
|
+
* target of an admitted test, which puts the test in its subject's component.
|
|
98
98
|
*/
|
|
99
|
-
function deadSubgraph(graph, dead, testOfDeadCode) {
|
|
99
|
+
function deadSubgraph(graph, dead, testOfDeadCode, unreferencedTest) {
|
|
100
100
|
const at = [];
|
|
101
101
|
const position = graph.symbols.map((_symbol, index) => {
|
|
102
|
-
|
|
103
|
-
if (dead[index] !== true || (graph.test[index] === true &&
|
|
102
|
+
const judged = testOfDeadCode[index] === true || unreferencedTest[index] === true;
|
|
103
|
+
if (dead[index] !== true || (graph.test[index] === true && !judged)) {
|
|
104
104
|
return UNVISITED;
|
|
105
105
|
}
|
|
106
106
|
at.push(index);
|
|
@@ -110,7 +110,7 @@ function deadSubgraph(graph, dead, testOfDeadCode) {
|
|
|
110
110
|
at.forEach((index, from) => {
|
|
111
111
|
for (const edge of graph.out[index] ?? []) {
|
|
112
112
|
const to = position[edge.to] ?? UNVISITED;
|
|
113
|
-
if (to === UNVISITED) {
|
|
113
|
+
if (to === UNVISITED || (unreferencedTest[index] === true && graph.test[edge.to] !== true)) {
|
|
114
114
|
continue;
|
|
115
115
|
}
|
|
116
116
|
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
|
};
|
|
@@ -32,6 +32,8 @@ const APP_DIRECTORY_ENTRIES = [
|
|
|
32
32
|
const SOLIDSTART_PLUGIN = { module: "@solidjs/start/config", export: "solidStart" };
|
|
33
33
|
const NUXT_CONFIG = [`nuxt.config.${ANY_MODULE}`];
|
|
34
34
|
const STRYKER_CONFIG = "{stryker,.stryker}.{conf,config}.{json,js,mjs,cjs}";
|
|
35
|
+
const STRYKER_MODULE = "{stryker,.stryker}.{conf,config}.{js,mjs,cjs}";
|
|
36
|
+
const VITEST_CONFIG = `{vitest,vite}.config.${ANY_MODULE}`;
|
|
35
37
|
/** Every row, ordered by name and then by range. */
|
|
36
38
|
export const CONVENTION_ROWS = [
|
|
37
39
|
{
|
|
@@ -146,6 +148,14 @@ export const CONVENTION_ROWS = [
|
|
|
146
148
|
},
|
|
147
149
|
],
|
|
148
150
|
},
|
|
151
|
+
{
|
|
152
|
+
name: "prettier",
|
|
153
|
+
package: "prettier",
|
|
154
|
+
range: ">=3.0.0 <4.0.0",
|
|
155
|
+
entries: [`{prettier.config,.prettierrc}.${ANY_MODULE}`],
|
|
156
|
+
moves: [],
|
|
157
|
+
manifestKeys: ["prettier"],
|
|
158
|
+
},
|
|
149
159
|
{
|
|
150
160
|
name: "qwik-city",
|
|
151
161
|
package: "@builder.io/qwik-city",
|
|
@@ -263,8 +273,9 @@ export const CONVENTION_ROWS = [
|
|
|
263
273
|
name: "stryker",
|
|
264
274
|
package: "@stryker-mutator/core",
|
|
265
275
|
range: ">=7.0.0 <11.0.0",
|
|
266
|
-
entries: [],
|
|
276
|
+
entries: [STRYKER_MODULE],
|
|
267
277
|
moves: [],
|
|
278
|
+
selections: [{ property: "mutate", files: [STRYKER_CONFIG] }],
|
|
268
279
|
shortNames: [
|
|
269
280
|
{
|
|
270
281
|
property: "testRunner",
|
|
@@ -408,9 +419,13 @@ export const CONVENTION_ROWS = [
|
|
|
408
419
|
shortNames: [
|
|
409
420
|
{
|
|
410
421
|
property: "test.coverage.provider",
|
|
411
|
-
files: [
|
|
422
|
+
files: [VITEST_CONFIG],
|
|
412
423
|
package: "@vitest/coverage-{}",
|
|
413
424
|
},
|
|
414
425
|
],
|
|
426
|
+
selections: [
|
|
427
|
+
{ property: "test.coverage.include", files: [VITEST_CONFIG] },
|
|
428
|
+
{ property: "test.coverage.exclude", files: [VITEST_CONFIG] },
|
|
429
|
+
],
|
|
415
430
|
},
|
|
416
431
|
];
|
package/dist/src/conventions.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { isArrayLiteralExpression, isAsExpression, isBinaryExpression, isCallExpression, isClassDeclaration, isComputedPropertyName, isExportAssignment, isExportDeclaration, isExpressionStatement, isFunctionDeclaration, isIdentifier, isImportDeclaration, isNamedExports, isNamespaceImport, isNoSubstitutionTemplateLiteral, isNonNullExpression, isNumericLiteral, isObjectBindingPattern, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isShorthandPropertyAssignment, isSpreadAssignment, isStringLiteral, isVariableStatement, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
9
9
|
import { lineOf } from "./component-files.js";
|
|
10
10
|
import { CONVENTION_ROWS, } from "./convention-rows.js";
|
|
11
|
+
import { documentStrings } from "./configuration-files.js";
|
|
11
12
|
import { globExpression } from "./glob.js";
|
|
12
13
|
import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from "./paths.js";
|
|
13
14
|
import { satisfies } from "./version-range.js";
|
|
@@ -349,6 +350,74 @@ function readPath(object, keys, values, elements) {
|
|
|
349
350
|
}
|
|
350
351
|
return undefined;
|
|
351
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* Every string literal one file writes at one property's path inside the objects it is
|
|
355
|
+
* read from, as {@link readProperty} reads them: the value itself, or each element of an
|
|
356
|
+
* array value, whatever else the path holds.
|
|
357
|
+
*/
|
|
358
|
+
function stringsAt(file, property, call) {
|
|
359
|
+
const declared = declaredValues(file);
|
|
360
|
+
const objects = call === undefined
|
|
361
|
+
? defaultExports(file).map((one) => objectOf(one, declared, true))
|
|
362
|
+
: optionsArguments(file, call).map((one) => one === undefined ? undefined : objectOf(one, declared, false));
|
|
363
|
+
const found = [];
|
|
364
|
+
const visit = (object, keys) => {
|
|
365
|
+
if (!isObjectLiteralExpression(object)) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const [key, ...rest] = keys;
|
|
369
|
+
for (const member of object.properties) {
|
|
370
|
+
if (!isPropertyAssignment(member) || keyOf(member) !== key) {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
const held = unwrapped(member.initializer);
|
|
374
|
+
if (rest.length > 0) {
|
|
375
|
+
visit(held, rest);
|
|
376
|
+
}
|
|
377
|
+
else if (literalText(held) !== undefined) {
|
|
378
|
+
found.push(held);
|
|
379
|
+
}
|
|
380
|
+
else if (isArrayLiteralExpression(held)) {
|
|
381
|
+
found.push(...held.elements.map(unwrapped).filter((one) => literalText(one) !== undefined));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
for (const object of objects) {
|
|
386
|
+
if (object !== undefined) {
|
|
387
|
+
visit(object, property.split("."));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return found;
|
|
391
|
+
}
|
|
392
|
+
/** The strings an applied row's selection keys hold, added per configuration file to `into`. */
|
|
393
|
+
function selectedStrings(host, parse, dir, readings, into) {
|
|
394
|
+
for (const reading of readings) {
|
|
395
|
+
for (const { name, file } of readingFiles(host, parse, dir, reading.files)) {
|
|
396
|
+
const path = joinPath(dir, name);
|
|
397
|
+
const held = into.get(path) ?? { starts: new Set(), texts: [] };
|
|
398
|
+
for (const node of stringsAt(file, reading.property, reading.call)) {
|
|
399
|
+
held.starts.add(node.getStart());
|
|
400
|
+
held.texts.push(literalText(node) ?? "");
|
|
401
|
+
}
|
|
402
|
+
into.set(path, held);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* The dependencies one manifest declares that a string at any depth of one of its members
|
|
408
|
+
* names, alone or followed by `/` and a subpath, a member's name aside.
|
|
409
|
+
*/
|
|
410
|
+
function manifestKeyUses(manifest, keys) {
|
|
411
|
+
if (!isRecord(manifest)) {
|
|
412
|
+
return [];
|
|
413
|
+
}
|
|
414
|
+
const declared = DEPENDENCY_SECTIONS.flatMap((section) => {
|
|
415
|
+
const held = manifest[section];
|
|
416
|
+
return isRecord(held) ? Object.keys(held) : [];
|
|
417
|
+
});
|
|
418
|
+
const strings = keys.flatMap((key) => documentStrings(manifest[key]));
|
|
419
|
+
return declared.filter((name) => strings.some((text) => text === name || text.startsWith(`${name}/`)));
|
|
420
|
+
}
|
|
352
421
|
/**
|
|
353
422
|
* Every value one file writes at one property's path inside the object it is read from:
|
|
354
423
|
* the options argument of every call `call` names, or the file's default export. A string
|
|
@@ -511,6 +580,7 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
|
|
|
511
580
|
const generated = [];
|
|
512
581
|
const failures = [];
|
|
513
582
|
const uses = new Set();
|
|
583
|
+
const selected = new Map();
|
|
514
584
|
for (const manifest of manifests) {
|
|
515
585
|
const declared = readJSON(host, joinPath(manifest.dir, "package.json"));
|
|
516
586
|
const versions = new Map();
|
|
@@ -571,6 +641,10 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
|
|
|
571
641
|
for (const name of shortNameUses(host, parse, manifest.dir, declared, row.shortNames ?? [])) {
|
|
572
642
|
uses.add(name);
|
|
573
643
|
}
|
|
644
|
+
for (const name of manifestKeyUses(declared, row.manifestKeys ?? [])) {
|
|
645
|
+
uses.add(name);
|
|
646
|
+
}
|
|
647
|
+
selectedStrings(host, parse, manifest.dir, row.selections ?? [], selected);
|
|
574
648
|
const excludes = [
|
|
575
649
|
...new Set((row.excludes ?? []).flatMap((one) => expand(one, resolved))),
|
|
576
650
|
].map(globExpression);
|
|
@@ -591,7 +665,14 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
|
|
|
591
665
|
unique.sort((a, b) => compare(a.name, b.name) ||
|
|
592
666
|
compare(a.manifest, b.manifest) ||
|
|
593
667
|
compare(JSON.stringify(a), JSON.stringify(b)));
|
|
594
|
-
return {
|
|
668
|
+
return {
|
|
669
|
+
applied: unique,
|
|
670
|
+
globs,
|
|
671
|
+
generated,
|
|
672
|
+
failures,
|
|
673
|
+
uses: [...uses].sort(compare),
|
|
674
|
+
selected,
|
|
675
|
+
};
|
|
595
676
|
}
|
|
596
677
|
/**
|
|
597
678
|
* Every own file of one project below a directory an applied row names as generated, by
|
package/dist/src/dependencies.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The dependencies the target's manifest
|
|
3
|
-
* need from outside the target. A file needs a package it
|
|
4
|
-
* augments or references the types of, by name or through the
|
|
5
|
-
* resolves to; a compiler configuration needs one it names, and a
|
|
6
|
-
* tool loads needs each dependency its strings spell. A package one
|
|
7
|
-
* for itself is that dependency's need, not the target's.
|
|
2
|
+
* The dependencies the target's manifest and its workspace members' manifests declare, and
|
|
3
|
+
* what the projects' own files need from outside the target. A file needs a package it
|
|
4
|
+
* imports, re-exports from, augments or references the types of, by name or through the
|
|
5
|
+
* file the specifier resolves to; a compiler configuration needs one it names, and a
|
|
6
|
+
* configuration file a tool loads needs each dependency its strings spell. A package one
|
|
7
|
+
* dependency pulls in for itself is that dependency's need, not the target's.
|
|
8
8
|
*/
|
|
9
9
|
import { isExportDeclaration, isExternalModuleReference, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, isNamespaceExport, isShorthandPropertyAssignment, isStringLiteral, isNoSubstitutionTemplateLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
10
10
|
import { configurationFiles, declaredIn as declaredBeside, dependenciesNamed, moduleStrings, } from "./configuration-files.js";
|
|
11
11
|
import { nodeKey, packageScope } from "./inventory.js";
|
|
12
|
-
import { dirnamePath, isAbsolutePath, joinPath, resolvePath } from "./paths.js";
|
|
12
|
+
import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.js";
|
|
13
13
|
import { UNANSWERED } from "./query.js";
|
|
14
14
|
import { sourceFilesOf } from "./source-files.js";
|
|
15
15
|
const MANIFEST = "package.json";
|
|
@@ -476,8 +476,8 @@ function installedOf(host, root, name) {
|
|
|
476
476
|
}
|
|
477
477
|
}
|
|
478
478
|
}
|
|
479
|
-
/** Every dependency the manifest text declares, in the order the text writes them. */
|
|
480
|
-
function declaredIn(text, manifest) {
|
|
479
|
+
/** Every dependency the manifest text at `path` declares, in the order the text writes them. */
|
|
480
|
+
function declaredIn(text, manifest, path, scope) {
|
|
481
481
|
const sections = membersAt(text, skipWhitespace(text, 0));
|
|
482
482
|
const declared = [];
|
|
483
483
|
for (const [member, section] of SECTIONS) {
|
|
@@ -489,7 +489,12 @@ function declaredIn(text, manifest) {
|
|
|
489
489
|
for (const [name, { key }] of membersAt(text, held.value)) {
|
|
490
490
|
const { line, column } = lineAndColumn(text, key);
|
|
491
491
|
declared.push({
|
|
492
|
-
dependency: {
|
|
492
|
+
dependency: {
|
|
493
|
+
name,
|
|
494
|
+
section,
|
|
495
|
+
position: { path, line, column, endLine: line },
|
|
496
|
+
manifest: scope,
|
|
497
|
+
},
|
|
493
498
|
at: key,
|
|
494
499
|
});
|
|
495
500
|
}
|
|
@@ -497,25 +502,36 @@ function declaredIn(text, manifest) {
|
|
|
497
502
|
return declared.sort((a, b) => a.at - b.at).map(({ dependency }) => dependency);
|
|
498
503
|
}
|
|
499
504
|
/**
|
|
500
|
-
* The run's dependency evidence: the target's manifest
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
* Only the manifest at the target root is read, as for entry points. A manifest the
|
|
505
|
+
* The run's dependency evidence: the target's manifest and each workspace member's in
|
|
506
|
+
* `members` below the target root, read with the position of every dependency each
|
|
507
|
+
* declares, the installed copy of each found from its manifest's directory, what the
|
|
508
|
+
* projects need, and the last uses each deletion candidate holds. A manifest the
|
|
505
509
|
* program cannot read declares nothing.
|
|
506
510
|
*/
|
|
507
|
-
export function dependenciesOf(host, targetRoot, perProject, candidates) {
|
|
508
|
-
const read = readJSON(host, joinPath(targetRoot, MANIFEST));
|
|
509
|
-
const manifest = isRecord(read?.value) ? read.value : undefined;
|
|
510
|
-
const scope = packageScope(host, targetRoot)(MANIFEST);
|
|
511
|
-
const declared = read !== undefined && manifest !== undefined ? declaredIn(read.text, manifest) : [];
|
|
511
|
+
export function dependenciesOf(host, targetRoot, perProject, candidates, members = []) {
|
|
512
512
|
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
513
|
+
const scopeOf = packageScope(host, targetRoot);
|
|
514
|
+
const manifests = new Map([[root, MANIFEST]]);
|
|
515
|
+
for (const dir of members) {
|
|
516
|
+
const below = relativePath(root, dir);
|
|
517
|
+
if (below !== undefined && !manifests.has(dir)) {
|
|
518
|
+
manifests.set(dir, `${below}/${MANIFEST}`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const declared = [];
|
|
513
522
|
const installed = new Map();
|
|
514
|
-
for (const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
523
|
+
for (const [dir, path] of manifests) {
|
|
524
|
+
const read = readJSON(host, joinPath(dir, MANIFEST));
|
|
525
|
+
if (read === undefined || !isRecord(read.value)) {
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
for (const dependency of declaredIn(read.text, read.value, path, scopeOf(path))) {
|
|
529
|
+
declared.push(dependency);
|
|
530
|
+
if (!installed.has(dependency.name)) {
|
|
531
|
+
const found = installedOf(host, dir, dependency.name);
|
|
532
|
+
if (found !== undefined) {
|
|
533
|
+
installed.set(dependency.name, found);
|
|
534
|
+
}
|
|
519
535
|
}
|
|
520
536
|
}
|
|
521
537
|
}
|
|
@@ -547,5 +563,5 @@ export function dependenciesOf(host, targetRoot, perProject, candidates) {
|
|
|
547
563
|
lastUses.set(id, sole.sort((a, b) => (a < b ? -1 : 1)));
|
|
548
564
|
}
|
|
549
565
|
}
|
|
550
|
-
return {
|
|
566
|
+
return { declared, installed, needed, lastUses };
|
|
551
567
|
}
|
package/dist/src/entry-points.js
CHANGED
|
@@ -17,10 +17,9 @@ import { globExpression } from "./glob.js";
|
|
|
17
17
|
import { htmlEntries } from "./html-entries.js";
|
|
18
18
|
import { dirnamePath, relativePath, resolvePath } from "./paths.js";
|
|
19
19
|
import { UNANSWERED } from "./query.js";
|
|
20
|
+
import { workflowEntries } from "./workflow-steps.js";
|
|
20
21
|
/** The extensions the test runner and the browser-test runner load a configuration from. */
|
|
21
22
|
const MODULE_EXTENSIONS = ["js", "mjs", "cjs", "ts", "mts", "cts"];
|
|
22
|
-
/** The extensions the mutation tester's configuration is loaded from. */
|
|
23
|
-
const SCRIPT_EXTENSIONS = ["js", "mjs", "cjs"];
|
|
24
23
|
/** The tool file names the configuration form does not cover. */
|
|
25
24
|
const CONVENTIONS = [
|
|
26
25
|
{
|
|
@@ -29,12 +28,6 @@ const CONVENTIONS = [
|
|
|
29
28
|
extensions: MODULE_EXTENSIONS,
|
|
30
29
|
testRunner: true,
|
|
31
30
|
},
|
|
32
|
-
{
|
|
33
|
-
rule: "mutation-testing",
|
|
34
|
-
stems: ["stryker.conf", ".stryker.config", ".stryker.conf"],
|
|
35
|
-
extensions: SCRIPT_EXTENSIONS,
|
|
36
|
-
testRunner: false,
|
|
37
|
-
},
|
|
38
31
|
];
|
|
39
32
|
/** The stem of the browser-test runner's configuration, whose test directory is read. */
|
|
40
33
|
const BROWSER_TEST_CONFIG = "playwright.config";
|
|
@@ -168,11 +161,29 @@ function isBrowserTestConfig(name) {
|
|
|
168
161
|
/**
|
|
169
162
|
* The files the configuration files of one project enter: each configuration module,
|
|
170
163
|
* with what it exports, and each own file a relative string of a configuration module
|
|
171
|
-
* or a JSON configuration file names, read against that file's directory.
|
|
164
|
+
* or a JSON configuration file names, read against that file's directory. A string at a
|
|
165
|
+
* selection key of an applied convention row names no file.
|
|
172
166
|
*/
|
|
173
|
-
function configurationEntries(host, targetRoot, files, parse) {
|
|
167
|
+
function configurationEntries(host, targetRoot, files, parse, selected) {
|
|
174
168
|
const { modules, documents, outside } = configurationFiles(host, targetRoot, files, parse);
|
|
175
169
|
const found = [];
|
|
170
|
+
const unselected = (file) => {
|
|
171
|
+
const starts = selected.get(file.fileName)?.starts;
|
|
172
|
+
return moduleStrings(file)
|
|
173
|
+
.filter((one) => starts?.has(one.node.getStart()) !== true)
|
|
174
|
+
.map((one) => one.text);
|
|
175
|
+
};
|
|
176
|
+
const unselectedIn = (path, texts) => {
|
|
177
|
+
const left = [...(selected.get(path)?.texts ?? [])];
|
|
178
|
+
return texts.filter((text) => {
|
|
179
|
+
const at = left.indexOf(text);
|
|
180
|
+
if (at < 0) {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
left.splice(at, 1);
|
|
184
|
+
return false;
|
|
185
|
+
});
|
|
186
|
+
};
|
|
176
187
|
const strings = (dir, texts) => {
|
|
177
188
|
for (const text of texts) {
|
|
178
189
|
for (const file of filesNamedBy(files, dir, text)) {
|
|
@@ -182,10 +193,10 @@ function configurationEntries(host, targetRoot, files, parse) {
|
|
|
182
193
|
};
|
|
183
194
|
for (const { file, form } of modules) {
|
|
184
195
|
found.push({ file, rule: "configuration-file", source: form, exports: true });
|
|
185
|
-
strings(dirnamePath(file.fileName),
|
|
196
|
+
strings(dirnamePath(file.fileName), unselected(file));
|
|
186
197
|
}
|
|
187
198
|
for (const document of documents) {
|
|
188
|
-
strings(dirnamePath(document.path), document.strings);
|
|
199
|
+
strings(dirnamePath(document.path), unselectedIn(document.path, document.strings));
|
|
189
200
|
}
|
|
190
201
|
for (const { file } of outside) {
|
|
191
202
|
strings(dirnamePath(file.fileName), moduleStrings(file).map((one) => one.text));
|
|
@@ -385,8 +396,8 @@ function workersOf(project, file, files) {
|
|
|
385
396
|
* file's path below the target root; a file one of them matches is a test file the
|
|
386
397
|
* runner executes.
|
|
387
398
|
*/
|
|
388
|
-
export function entryPoints(project, files, testFiles, host, targetRoot, parse) {
|
|
389
|
-
const found = [...configurationEntries(host, targetRoot, files, parse)];
|
|
399
|
+
export function entryPoints(project, files, testFiles, host, targetRoot, parse, selected = new Map()) {
|
|
400
|
+
const found = [...configurationEntries(host, targetRoot, files, parse, selected)];
|
|
390
401
|
const tests = testFiles.map((pattern) => ({ pattern, expression: globExpression(pattern) }));
|
|
391
402
|
for (const [path, file] of files.byPath) {
|
|
392
403
|
const name = basename(path);
|
|
@@ -410,5 +421,8 @@ export function entryPoints(project, files, testFiles, host, targetRoot, parse)
|
|
|
410
421
|
for (const { file, source } of htmlEntries(host, targetRoot, files, parse)) {
|
|
411
422
|
found.push({ file, rule: "html-entry", source, exports: true });
|
|
412
423
|
}
|
|
424
|
+
for (const { file, source } of workflowEntries(host, targetRoot, files)) {
|
|
425
|
+
found.push({ file, rule: "workflow-step", source, exports: true });
|
|
426
|
+
}
|
|
413
427
|
return found;
|
|
414
428
|
}
|
package/dist/src/exempt.js
CHANGED
|
@@ -117,6 +117,7 @@ export function evidenceRecords(input, detectors, disabled) {
|
|
|
117
117
|
detail: evidence.detail,
|
|
118
118
|
site: evidence.site,
|
|
119
119
|
...(evidence.whileLive === undefined ? {} : { whileLive: evidence.whileLive }),
|
|
120
|
+
...(evidence.implementing === undefined ? {} : { implementing: evidence.implementing }),
|
|
120
121
|
});
|
|
121
122
|
}
|
|
122
123
|
}
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
import { componentId } from "../components.js";
|
|
7
7
|
import { OUTSIDE } from "../graph.js";
|
|
8
8
|
import { KINDS } from "../kinds.js";
|
|
9
|
-
import { byPosition } from "../position.js";
|
|
9
|
+
import { byPosition, positionKey } from "../position.js";
|
|
10
|
+
import { declarationKey } from "../ref.js";
|
|
10
11
|
import { FAMILY_KEY_LENGTH } from "../resolve.js";
|
|
11
12
|
import { isPartSubject, isRowSubject } from "../suppress.js";
|
|
12
13
|
/** The classes ranked by the claim each makes, the strongest highest. */
|
|
@@ -117,7 +118,7 @@ function findingComponents(swept, cascade) {
|
|
|
117
118
|
export function completed(input, findings, kinds = KINDS) {
|
|
118
119
|
const { swept } = input;
|
|
119
120
|
const union = swept.matrix.union;
|
|
120
|
-
const positions = new Map(union.symbols.map((symbol, at) => [symbol.ref, at]));
|
|
121
|
+
const positions = new Map(union.symbols.map((symbol, at) => [declarationKey(symbol.ref, symbol.position.path), at]));
|
|
121
122
|
const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
|
|
122
123
|
const classOf = reachabilityClasses(input);
|
|
123
124
|
const componentOf = findingComponents(swept, input.config.reporters.cascade);
|
|
@@ -135,10 +136,14 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
135
136
|
throw new Error(`${finding.code} names no live row of the issue-kind vocabulary`);
|
|
136
137
|
}
|
|
137
138
|
// A row's reference may spell a declaration's, as an ignore entry naming one does, and
|
|
138
|
-
// a row is still never the declaration.
|
|
139
|
+
// a row is still never the declaration. Two declarations of one file can share a
|
|
140
|
+
// reference, so the one the finding is positioned at is the subject where there is one.
|
|
141
|
+
const declared = union.at(positionKey(finding.position));
|
|
139
142
|
const at = isRowSubject(finding.symbol.kind)
|
|
140
143
|
? OUTSIDE
|
|
141
|
-
:
|
|
144
|
+
: union.symbols[declared]?.ref === finding.symbol.ref
|
|
145
|
+
? declared
|
|
146
|
+
: (positions.get(declarationKey(finding.symbol.ref, finding.position.path)) ?? OUTSIDE);
|
|
142
147
|
// A part names the declaration that holds it, and is decided inside it rather than by
|
|
143
148
|
// the declaration's liveness, so it carries none of the declaration's sweep facts.
|
|
144
149
|
const part = isPartSubject(finding.symbol.kind);
|
|
@@ -30,7 +30,7 @@ function neededBy(dependencies) {
|
|
|
30
30
|
return needed;
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
|
-
* A dependency, development dependency or peer dependency of the
|
|
33
|
+
* A dependency, development dependency or peer dependency of a manifest the run reads
|
|
34
34
|
* that the target does not need, at the position of its key.
|
|
35
35
|
*/
|
|
36
36
|
function unusedDependencies(dependencies) {
|
|
@@ -41,7 +41,7 @@ function unusedDependencies(dependencies) {
|
|
|
41
41
|
code: UNUSED_DEPENDENCY,
|
|
42
42
|
position: dependency.position,
|
|
43
43
|
symbol: {
|
|
44
|
-
ref: renderRef(
|
|
44
|
+
ref: renderRef(dependency.manifest, {
|
|
45
45
|
of: "dependency",
|
|
46
46
|
name: dependency.name,
|
|
47
47
|
section: dependency.section,
|