@cplieger/deadset-ts 3.13.0 → 4.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 +67 -49
- package/dist/bin/node-host.js +10 -7
- package/dist/src/alias-chain.js +59 -7
- package/dist/src/analysis.js +120 -24
- package/dist/src/calls.js +23 -22
- package/dist/src/class-members.js +10 -11
- package/dist/src/configured-declarations.js +22 -9
- package/dist/src/dependencies.js +34 -17
- package/dist/src/deprecation.js +35 -24
- package/dist/src/destructuring.js +37 -29
- package/dist/src/discover.js +106 -29
- package/dist/src/entry-point-gaps.js +2 -2
- package/dist/src/entry-points.js +7 -0
- package/dist/src/enum-group.js +46 -40
- package/dist/src/exempt.js +42 -1
- package/dist/src/findings/emitters.js +15 -1
- package/dist/src/findings/interfaces.js +7 -1
- package/dist/src/framework-lifecycle.js +8 -6
- package/dist/src/implementations.js +45 -23
- package/dist/src/interface-satisfaction.js +30 -27
- package/dist/src/intra-function-parts.js +37 -20
- package/dist/src/inventory.js +85 -4
- package/dist/src/json-chunks.js +76 -0
- package/dist/src/partial.js +23 -0
- package/dist/src/query.js +207 -0
- package/dist/src/references.js +92 -20
- package/dist/src/reflective-lookup.js +18 -9
- package/dist/src/report.js +3 -1
- package/dist/src/reporters/annotations.js +9 -5
- package/dist/src/reporters/json.js +3 -2
- package/dist/src/reporters/sarif.js +2 -1
- package/dist/src/reporters/template.js +51 -10
- package/dist/src/reporters/text.js +8 -6
- package/dist/src/roots.js +27 -11
- package/dist/src/schema.js +2 -2
- package/dist/src/serialization-contract.js +12 -10
- package/dist/src/session.js +79 -39
- package/dist/src/suppress-file.js +2 -1
- package/dist/src/sweep.js +4 -1
- package/dist/src/value-reach.js +14 -19
- package/dist/src/verbs/analyze.js +20 -3
- package/dist/src/verbs/explain.js +22 -9
- package/dist/src/verbs/print-projects.js +41 -8
- package/dist/src/verbs/print-retained.js +6 -2
- package/dist/src/verbs/print-roots.js +5 -1
- package/jsr.json +1 -1
- package/package.json +3 -3
- package/src/alias-chain.ts +75 -11
- package/src/analysis.ts +224 -62
- package/src/calls.ts +10 -9
- package/src/class-members.ts +10 -11
- package/src/configured-declarations.ts +29 -13
- package/src/dependencies.ts +42 -20
- package/src/deprecation.ts +53 -27
- package/src/destructuring.ts +50 -39
- package/src/discover.ts +178 -34
- package/src/entry-point-gaps.ts +2 -2
- package/src/entry-points.ts +7 -0
- package/src/enum-group.ts +56 -42
- package/src/exempt.ts +50 -2
- package/src/findings/emitters.ts +18 -1
- package/src/findings/interfaces.ts +7 -1
- package/src/framework-lifecycle.ts +10 -6
- package/src/host.ts +5 -3
- package/src/implementations.ts +59 -23
- package/src/interface-satisfaction.ts +30 -27
- package/src/intra-function-parts.ts +43 -25
- package/src/inventory.ts +97 -4
- package/src/json-chunks.ts +87 -0
- package/src/partial.ts +40 -0
- package/src/query.ts +522 -0
- package/src/references.ts +119 -38
- package/src/reflective-lookup.ts +21 -11
- package/src/report.ts +14 -2
- package/src/reporters/annotations.ts +23 -21
- package/src/reporters/json.ts +3 -2
- package/src/reporters/reporter.ts +2 -2
- package/src/reporters/sarif.ts +3 -2
- package/src/reporters/template.ts +53 -11
- package/src/reporters/text.ts +12 -12
- package/src/roots.ts +34 -12
- package/src/schema.ts +2 -2
- package/src/serialization-contract.ts +12 -10
- package/src/session.ts +146 -58
- package/src/suppress-file.ts +6 -2
- package/src/sweep.ts +9 -4
- package/src/value-reach.ts +20 -23
- package/src/verbs/analyze.ts +25 -3
- package/src/verbs/explain.ts +20 -9
- package/src/verbs/print-projects.ts +54 -8
- package/src/verbs/print-retained.ts +6 -2
- package/src/verbs/print-roots.ts +5 -1
package/README.md
CHANGED
|
@@ -2,13 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@cplieger/deadset-ts) [](https://jsr.io/@cplieger/deadset-ts)
|
|
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
|
-
|
|
7
|
+
It reads your `tsconfig.json` projects, `allowJs` JavaScript included, with the type information of TypeScript 7.1.0-dev.20261003.1, its one dependency. Your project keeps its own TypeScript version and must also compile under that version. 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
|
|
|
11
|
-
|
|
11
|
+
deadset-ts is built for a CI gate on dead code. Each finding names code to delete, an `export` to drop or a setting to fix.
|
|
12
|
+
|
|
13
|
+
- It reports unused exports, files and dependencies, and unused members of classes, interfaces, type aliases, enums and namespaces, private and static included.
|
|
14
|
+
- Inside a function, it reports unused parameters and results, unreachable statements and cases, and dead stores.
|
|
15
|
+
- It reports an `export` only its own file uses, and declarations that only reference each other.
|
|
16
|
+
- It keeps what decorators, reflective lookups, `JSON.stringify` and the frameworks, containers, serializers and templates you name in `deadset.json` use.
|
|
17
|
+
- The same tree gives the same report, and a baseline keyed on code and symbol survives moved lines.
|
|
18
|
+
- It writes text, JSON, GitHub annotations, SARIF 2.1.0 or a custom template.
|
|
19
|
+
|
|
20
|
+
Consider [Knip](https://knip.dev) if you want issues fixed with `--fix`, plugins for over 100 tools, or Astro, MDX, Svelte and Vue files checked.
|
|
12
21
|
|
|
13
22
|
## Install
|
|
14
23
|
|
|
@@ -20,70 +29,79 @@ npx jsr add -D @cplieger/deadset-ts
|
|
|
20
29
|
npx @cplieger/deadset-ts version
|
|
21
30
|
```
|
|
22
31
|
|
|
23
|
-
The npm package carries the command and the library as JavaScript that the compiler emits at publish, so `npx deadset-ts` and `import("@cplieger/deadset-ts")` both run on plain Node.js 24 or later, with no loader and no flag. It carries the TypeScript source as well, which is what a TypeScript consumer's own compiler resolves. That consumer sets `allowImportingTsExtensions`, because the source names each import with its `.ts` extension. The JSR package ships source only, and Deno compiles it: a Deno program imports and type-checks the library, but the compiler's synchronous session does not open under Deno, so running an analysis needs Node.
|
|
24
|
-
|
|
25
32
|
## Usage
|
|
26
33
|
|
|
34
|
+
Create a `deadset.json` at the project root that says whether the project is an `application` or a `library`, then run `analyze`. A run reads every `tsconfig` project under that folder, project references included, and reports a declaration only when it is dead in all of them.
|
|
35
|
+
|
|
27
36
|
```sh
|
|
28
|
-
|
|
37
|
+
echo '{ "target": { "kind": "application" } }' > deadset.json
|
|
38
|
+
npx @cplieger/deadset-ts analyze --report=deadset-report.json
|
|
29
39
|
```
|
|
30
40
|
|
|
31
|
-
`
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
`analyze` writes the JSON report to `deadset-report.json` and a text rendering beside it as `deadset-report.json.txt`. Take this `src/greet.ts`, where the rest of the project calls only `greet` and `Counter.increment`:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
export function greet(name: string): string {
|
|
45
|
+
return `hello ${name}`;
|
|
46
|
+
}
|
|
47
|
+
export function farewell(name: string): string {
|
|
48
|
+
return `bye ${name}`;
|
|
49
|
+
}
|
|
50
|
+
export class Counter {
|
|
51
|
+
private count = 0;
|
|
52
|
+
increment(): void {
|
|
53
|
+
this.count += 1;
|
|
54
|
+
}
|
|
55
|
+
reset(): void {
|
|
56
|
+
this.count = 0;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
49
60
|
|
|
50
|
-
|
|
61
|
+
The text rendering lists three findings:
|
|
51
62
|
|
|
52
|
-
|
|
63
|
+
```text
|
|
64
|
+
src/greet.ts:4:17: function farewell: exported function has no reference in the target and none from any loaded consumer [certain] (DS1001)
|
|
65
|
+
src/greet.ts:8:11: class-member Counter.count: member Counter.count is written at 2 positions and never read [certain] (DS1301)
|
|
66
|
+
src/greet.ts:12:3: method Counter.reset: method has no reference in the target [certain] (DS1003)
|
|
67
|
+
summary: 3 findings (0 allow, 0 warn, 3 deny), 6 deletable lines, 0 suppressions in effect, 0 reasons recorded, 0 stale suppressions, 0 pending, 0 omitted
|
|
68
|
+
```
|
|
53
69
|
|
|
54
|
-
The
|
|
70
|
+
The run exits 1 because the report holds a finding at `deny` severity, and 0 when it holds none. Three common next steps:
|
|
55
71
|
|
|
56
|
-
- `
|
|
57
|
-
- `
|
|
58
|
-
-
|
|
72
|
+
- Add `--format=sarif` or `--format=github` for a SARIF file or GitHub workflow annotations. Naming any format turns off the default text rendering, so add `--format=text` as well to keep it.
|
|
73
|
+
- Run `npx @cplieger/deadset-ts explain --why=farewell` to see why one declaration is reported, retained, held by an unanswered question, live or dead.
|
|
74
|
+
- Add `--baseline-write=deadset-baseline.json` to record today's findings. Later runs fail on a new finding and on a baseline row whose finding has gone. To keep one declaration, put `// deadset:ignore DS1001 -- <reason>` on the line above it.
|
|
59
75
|
|
|
60
|
-
|
|
76
|
+
[Commands and exit codes](docs/commands.md) covers every verb and option, and [Configuration](docs/configuration.md) covers every setting.
|
|
61
77
|
|
|
62
|
-
|
|
63
|
-
- `DirectoryEntry`, `PathKind`: one entry of a directory read, and what one path names.
|
|
78
|
+
## API
|
|
64
79
|
|
|
65
|
-
|
|
80
|
+
The package exports the command line as a function, and the helpers that spell the positions and symbol references in a report.
|
|
66
81
|
|
|
67
|
-
- `
|
|
68
|
-
- `
|
|
69
|
-
- `
|
|
70
|
-
- `
|
|
82
|
+
- `run` runs the command line and returns its exit code, without exiting the process. `Writer` is its output stream, and `SETTING_OPTIONS` maps each option to the setting it sets.
|
|
83
|
+
- `Host`, `DirectoryEntry` and `PathKind` describe the filesystem, working directory and version a run reads.
|
|
84
|
+
- `renderPosition`, `positionKey`, `byPosition`, `PositionError` and `Position` render and order positions.
|
|
85
|
+
- `renderRef`, `nameComponent`, `computedComponent`, `isRef`, `REF_EXPRESSIONS`, `Module`, `Component`, `Fragment` and `DependencySection` spell stable symbol references.
|
|
86
|
+
- `DECLINED_CONVENTIONS` and `DeclinedConvention` list the entry-point conventions of other tools the analysis does not read.
|
|
87
|
+
- `CONTRACT_VERSION` is the version of the [deadset Contract](https://github.com/cplieger/deadset-spec) this analyzer implements.
|
|
71
88
|
|
|
72
|
-
|
|
89
|
+
A TypeScript caller that imports the source sets `allowImportingTsExtensions`, because each import in it ends in `.ts`. [Using deadset-ts as a library](docs/library.md) covers the packages and in-process runs, and [JSR](https://jsr.io/@cplieger/deadset-ts/doc) has the full reference.
|
|
73
90
|
|
|
74
|
-
|
|
75
|
-
- `nameComponent(name)`, `computedComponent(text)`: the canonical spelling of one component of that reference, from a declared name or from a computed key.
|
|
76
|
-
- `isRef(value)`: whether one string is a reference of this language's grammar.
|
|
77
|
-
- `REF_EXPRESSIONS`: one regular expression per form the grammar defines.
|
|
78
|
-
- `Module`, `Component`, `Fragment`, `DependencySection`: the module half of a reference, one component of its fragment, what the fragment names, and the manifest section a dependency was declared in.
|
|
91
|
+
## Related projects
|
|
79
92
|
|
|
80
|
-
|
|
93
|
+
deadset-ts implements the [deadset Contract](https://github.com/cplieger/deadset-spec), which fixes the issue codes, the report schema and the exit codes. It passes the Contract's conformance corpus except one reflective-lookup fixture, the gap [`conformance.json`](conformance.json) declares.
|
|
81
94
|
|
|
82
|
-
-
|
|
95
|
+
- [deadset-go](https://github.com/cplieger/deadset-go) is the same analysis for Go modules.
|
|
96
|
+
- [deadset](https://github.com/cplieger/deadset) runs both analyzers as one command and merges their reports, resolving the edges between Go and TypeScript code.
|
|
83
97
|
|
|
84
|
-
|
|
98
|
+
## Documentation
|
|
85
99
|
|
|
86
|
-
-
|
|
100
|
+
- [Commands and exit codes](docs/commands.md) lists every verb, option, rendering and exit code.
|
|
101
|
+
- [Configuration](docs/configuration.md) lists every setting, the suppression files and the scope document.
|
|
102
|
+
- [How deadset-ts works](docs/how-it-works.md) explains the projects it loads, the roots it starts from and what it holds back.
|
|
103
|
+
- [Using deadset-ts as a library](docs/library.md) covers the npm and JSR packages and running the analyzer in-process.
|
|
104
|
+
- [Batch-cap calibration](docs/batch-cap-calibration.md) is the measurement behind the batch size of the reference pass.
|
|
87
105
|
|
|
88
106
|
## Contributing
|
|
89
107
|
|
package/dist/bin/node-host.js
CHANGED
|
@@ -47,18 +47,21 @@ function readVersion() {
|
|
|
47
47
|
/** The mode every document the command writes carries. */
|
|
48
48
|
const DOCUMENT_MODE = 0o644;
|
|
49
49
|
/**
|
|
50
|
-
* Writes one document through a temporary file in the same directory,
|
|
51
|
-
* renamed into place, so a run that dies partway leaves no truncated
|
|
50
|
+
* Writes one document through a temporary file in the same directory, piece by piece,
|
|
51
|
+
* flushed and then renamed into place, so a run that dies partway leaves no truncated
|
|
52
|
+
* document at the path.
|
|
52
53
|
*/
|
|
53
|
-
function writeAtomically(path,
|
|
54
|
+
function writeAtomically(path, pieces) {
|
|
54
55
|
const temporary = join(dirname(path), `.${basename(path)}.${randomBytes(6).toString("hex")}`);
|
|
55
56
|
const fd = openSync(temporary, "wx", DOCUMENT_MODE);
|
|
56
57
|
try {
|
|
57
58
|
try {
|
|
58
59
|
fchmodSync(fd, DOCUMENT_MODE);
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
written
|
|
60
|
+
for (const piece of pieces) {
|
|
61
|
+
const bytes = Buffer.from(piece, "utf8");
|
|
62
|
+
for (let written = 0; written < bytes.length;) {
|
|
63
|
+
written += writeSync(fd, bytes, written);
|
|
64
|
+
}
|
|
62
65
|
}
|
|
63
66
|
fsyncSync(fd);
|
|
64
67
|
}
|
|
@@ -99,6 +102,6 @@ export function nodeHost() {
|
|
|
99
102
|
found ??= readVersion();
|
|
100
103
|
return found;
|
|
101
104
|
},
|
|
102
|
-
|
|
105
|
+
writeDocument: writeAtomically,
|
|
103
106
|
};
|
|
104
107
|
}
|
package/dist/src/alias-chain.js
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
* declaration of the target it is, and along the chain of aliases it heads to each
|
|
4
4
|
* declaration every link of that chain is.
|
|
5
5
|
*/
|
|
6
|
+
import { isExportAssignment, isExportSpecifier, isIdentifier, isImportClause, isImportSpecifier, isNamespaceExport, isNamespaceImport, } from "@typescript/native/unstable/ast";
|
|
6
7
|
import { SymbolFlags } from "@typescript/native/unstable/sync";
|
|
7
|
-
import { nodeKey } from "./inventory.js";
|
|
8
|
+
import { declarationsByName, nodeKey } from "./inventory.js";
|
|
9
|
+
import { UNANSWERED } from "./query.js";
|
|
8
10
|
/**
|
|
9
11
|
* The chain walks over one project, against the declarations of `held`, the same
|
|
10
12
|
* project's inventory.
|
|
@@ -14,7 +16,7 @@ import { nodeKey } from "./inventory.js";
|
|
|
14
16
|
* library file is never fetched to find out that the inventory does not hold it.
|
|
15
17
|
*/
|
|
16
18
|
export function aliasChains(project, held) {
|
|
17
|
-
const ownFiles =
|
|
19
|
+
const ownFiles = project.ownPaths();
|
|
18
20
|
const steps = new Map();
|
|
19
21
|
const chains = new Map();
|
|
20
22
|
let asked = 0;
|
|
@@ -50,23 +52,68 @@ export function aliasChains(project, held) {
|
|
|
50
52
|
steps.set(symbol.id, next);
|
|
51
53
|
return next;
|
|
52
54
|
};
|
|
55
|
+
const files = held.symbols.filter((one) => one.kind === "file").map((one) => one.id);
|
|
56
|
+
/**
|
|
57
|
+
* The declarations an alias whose step went unanswered may stand for: every one the
|
|
58
|
+
* inventory holds under its own name or the name it carries forward, and every file
|
|
59
|
+
* where it carries a module, so a use through it still counts for what it reaches.
|
|
60
|
+
*/
|
|
61
|
+
const sameNamed = (alias) => {
|
|
62
|
+
const names = new Set([alias.name]);
|
|
63
|
+
let module = false;
|
|
64
|
+
for (const handle of alias.declarations) {
|
|
65
|
+
const node = ownFiles.has(handle.path) ? project.declarationAt(handle)?.node : undefined;
|
|
66
|
+
if (node === undefined) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if ((isImportSpecifier(node) || isExportSpecifier(node)) && node.propertyName !== undefined) {
|
|
70
|
+
names.add(node.propertyName.text);
|
|
71
|
+
}
|
|
72
|
+
else if (isImportClause(node)) {
|
|
73
|
+
names.add("default");
|
|
74
|
+
}
|
|
75
|
+
else if (isNamespaceImport(node) || isNamespaceExport(node)) {
|
|
76
|
+
module = true;
|
|
77
|
+
}
|
|
78
|
+
else if (isExportAssignment(node) && isIdentifier(node.expression)) {
|
|
79
|
+
names.add(node.expression.text);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const byName = declarationsByName(held);
|
|
83
|
+
const ids = [...names].flatMap((name) => byName.get(name) ?? []);
|
|
84
|
+
return [...new Set([...ids, ...(module ? files : [])])].map((id) => ({
|
|
85
|
+
id,
|
|
86
|
+
stepped: true,
|
|
87
|
+
guessed: true,
|
|
88
|
+
}));
|
|
89
|
+
};
|
|
53
90
|
const chainOf = (symbol) => {
|
|
54
91
|
const cached = chains.get(symbol.id);
|
|
55
92
|
if (cached !== undefined) {
|
|
56
93
|
return cached;
|
|
57
94
|
}
|
|
58
95
|
const targets = [];
|
|
96
|
+
const add = (id, stepped, guessed = false) => {
|
|
97
|
+
if (!targets.some((target) => target.id === id)) {
|
|
98
|
+
targets.push({ id, stepped, guessed });
|
|
99
|
+
}
|
|
100
|
+
};
|
|
59
101
|
const walked = new Set();
|
|
60
102
|
let at = symbol;
|
|
61
103
|
let stepped = false;
|
|
62
104
|
while (at !== undefined && !walked.has(at.id)) {
|
|
63
105
|
walked.add(at.id);
|
|
64
106
|
for (const id of declarationsOf(at)) {
|
|
65
|
-
|
|
66
|
-
|
|
107
|
+
add(id, stepped);
|
|
108
|
+
}
|
|
109
|
+
const next = stepOf(at);
|
|
110
|
+
if (next === UNANSWERED) {
|
|
111
|
+
for (const target of sameNamed(at)) {
|
|
112
|
+
add(target.id, true, true);
|
|
67
113
|
}
|
|
114
|
+
break;
|
|
68
115
|
}
|
|
69
|
-
at =
|
|
116
|
+
at = next;
|
|
70
117
|
stepped = true;
|
|
71
118
|
}
|
|
72
119
|
chains.set(symbol.id, targets);
|
|
@@ -74,13 +121,18 @@ export function aliasChains(project, held) {
|
|
|
74
121
|
};
|
|
75
122
|
const nextDeclared = (symbol) => {
|
|
76
123
|
const walked = new Set([symbol.id]);
|
|
124
|
+
let from = symbol;
|
|
77
125
|
let at = stepOf(symbol);
|
|
78
|
-
while (at !== undefined && !walked.has(at.id)) {
|
|
126
|
+
while (at !== undefined && (at === UNANSWERED || !walked.has(at.id))) {
|
|
127
|
+
if (at === UNANSWERED) {
|
|
128
|
+
return sameNamed(from);
|
|
129
|
+
}
|
|
79
130
|
walked.add(at.id);
|
|
80
131
|
const ids = declarationsOf(at);
|
|
81
132
|
if (ids.length > 0) {
|
|
82
|
-
return ids;
|
|
133
|
+
return ids.map((id) => ({ id, stepped: true, guessed: false }));
|
|
83
134
|
}
|
|
135
|
+
from = at;
|
|
84
136
|
at = stepOf(at);
|
|
85
137
|
}
|
|
86
138
|
return [];
|
package/dist/src/analysis.js
CHANGED
|
@@ -12,10 +12,10 @@ import { configuredDeclarations, configuredRef, declarationsNamed, } from "./con
|
|
|
12
12
|
import { decorator } from "./decorator.js";
|
|
13
13
|
import { dependenciesOf, projectNeeds } from "./dependencies.js";
|
|
14
14
|
import { deprecatedDeclarations } from "./deprecation.js";
|
|
15
|
-
import {
|
|
15
|
+
import { discoverProjects, DiscoveryError, isGuess, projectErrors, renderDiagnostic, targetRelative, } from "./discover.js";
|
|
16
16
|
import { readEdgeSides } from "./edges.js";
|
|
17
17
|
import { enumGroup } from "./enum-group.js";
|
|
18
|
-
import { computeExemptions, disabledClasses, exemptionsOf, retainedIn, } from "./exempt.js";
|
|
18
|
+
import { computeExemptions, disabledClasses, exemptionsOf, isUnansweredRecord, retainedIn, } from "./exempt.js";
|
|
19
19
|
import { heldByInclusion, sourceTree } from "./file-facts.js";
|
|
20
20
|
import { findingsPass, recordedFindings, rowKey } from "./findings-pass.js";
|
|
21
21
|
import { NO_SELF_CHECK, unmatchedRootFindings, } from "./findings/self-check.js";
|
|
@@ -63,12 +63,12 @@ function compare(a, b) {
|
|
|
63
63
|
return a < b ? -1 : 1;
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
|
-
* Reads every project the scope discovers, and every project of
|
|
67
|
-
* declares, in one snapshot
|
|
68
|
-
* enumerated
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
66
|
+
* Reads every project the scope discovers, and every project of each consumer it
|
|
67
|
+
* declares, in one snapshot, each checked for errors first. A target project is
|
|
68
|
+
* enumerated, rooted and handed to `stage`; a consumer project is read for its references
|
|
69
|
+
* to the target. A guess that does not load is dropped into `notBuilt` (see
|
|
70
|
+
* {@link isGuess}); any other project that does not load or carries an error fails the
|
|
71
|
+
* run, as does a run whose every guess was dropped.
|
|
72
72
|
*/
|
|
73
73
|
function readProjects(engine, host, scope, config, stage) {
|
|
74
74
|
const targetRoot = scope.target.path;
|
|
@@ -83,15 +83,31 @@ function readProjects(engine, host, scope, config, stage) {
|
|
|
83
83
|
testFiles: config.ts.testFiles,
|
|
84
84
|
publishedAPI: config.targetKind === "library",
|
|
85
85
|
};
|
|
86
|
+
const absoluteRoot = resolvePath(host.workingDirectory(), targetRoot);
|
|
87
|
+
const notBuilt = [...discovered.notBuilt];
|
|
88
|
+
const dropped = (configFile, error) => {
|
|
89
|
+
notBuilt.push({
|
|
90
|
+
id: ids.get(configFile) ?? configFile,
|
|
91
|
+
configFile,
|
|
92
|
+
error: targetRelative(error, absoluteRoot),
|
|
93
|
+
});
|
|
94
|
+
};
|
|
86
95
|
const failures = [];
|
|
87
96
|
const consumed = new Map();
|
|
88
|
-
const
|
|
89
|
-
const errors =
|
|
90
|
-
|
|
91
|
-
|
|
97
|
+
const session = runSession(engine, [...discovered.configFiles, ...consumerOf.keys()], (project) => {
|
|
98
|
+
const errors = projectErrors(diagnosticsOf(project));
|
|
99
|
+
const consumer = consumerOf.get(project.configFile);
|
|
100
|
+
const [unloaded] = errors.load;
|
|
101
|
+
if (unloaded !== undefined &&
|
|
102
|
+
consumer === undefined &&
|
|
103
|
+
isGuess(discovered, project.configFile)) {
|
|
104
|
+
dropped(project.configFile, renderDiagnostic(unloaded, (path) => relativePath(absoluteRoot, path) ?? path));
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
if (errors.load.length > 0 || errors.check.length > 0) {
|
|
108
|
+
failures.push(...errors.load, ...errors.check);
|
|
92
109
|
return undefined;
|
|
93
110
|
}
|
|
94
|
-
const consumer = consumerOf.get(project.configFile);
|
|
95
111
|
if (consumer !== undefined) {
|
|
96
112
|
for (const reference of consumerReferences(project, host, targetRoot, {
|
|
97
113
|
id: consumer.id,
|
|
@@ -105,16 +121,29 @@ function readProjects(engine, host, scope, config, stage) {
|
|
|
105
121
|
const rooted = roots(project, held, targetRoot, options);
|
|
106
122
|
const configuration = ids.get(project.configFile) ?? project.configFile;
|
|
107
123
|
return stage(project, { configuration, configFile: project.configFile, held, rooted });
|
|
108
|
-
});
|
|
124
|
+
}, discovered.derived ? "record" : "refuse");
|
|
125
|
+
for (const configFile of session.unopened) {
|
|
126
|
+
if (consumerOf.has(configFile) || !isGuess(discovered, configFile)) {
|
|
127
|
+
throw new DiscoveryError(`${configFile} opened no project`, []);
|
|
128
|
+
}
|
|
129
|
+
dropped(configFile, "the compiler opened no project for the configuration");
|
|
130
|
+
}
|
|
109
131
|
if (failures.length > 0) {
|
|
110
|
-
throw new DiscoveryError(`${String(failures.length)} error(s)
|
|
132
|
+
throw new DiscoveryError(`${String(failures.length)} error(s), so no answer was produced`, failures);
|
|
133
|
+
}
|
|
134
|
+
const answers = session.projects.filter((answer) => answer !== undefined);
|
|
135
|
+
const [first] = notBuilt;
|
|
136
|
+
if (answers.length === 0 && first !== undefined) {
|
|
137
|
+
throw new DiscoveryError(`no configuration discovery derived could be built; ${first.id}: ${first.error}`, []);
|
|
111
138
|
}
|
|
112
139
|
return {
|
|
113
|
-
answers
|
|
140
|
+
answers,
|
|
114
141
|
consumers: {
|
|
115
142
|
loaded: loads.map((load) => load.consumer),
|
|
116
143
|
references: [...consumed.values()],
|
|
117
144
|
},
|
|
145
|
+
notBuilt,
|
|
146
|
+
unanswered: session.unanswered,
|
|
118
147
|
};
|
|
119
148
|
}
|
|
120
149
|
/**
|
|
@@ -125,7 +154,8 @@ function readProjects(engine, host, scope, config, stage) {
|
|
|
125
154
|
*/
|
|
126
155
|
export function runRoots(engine, host, scope, config, provenance) {
|
|
127
156
|
const targetRoot = scope.target.path;
|
|
128
|
-
const
|
|
157
|
+
const read = readProjects(engine, host, scope, config, (_project, projectRead) => projectRead);
|
|
158
|
+
const projects = read.answers;
|
|
129
159
|
const configurations = [];
|
|
130
160
|
const merged = new Map();
|
|
131
161
|
for (const answer of projects) {
|
|
@@ -159,6 +189,8 @@ export function runRoots(engine, host, scope, config, provenance) {
|
|
|
159
189
|
configurations,
|
|
160
190
|
roots: ordered,
|
|
161
191
|
findings: unmatchedRootFindings(unmatched, documentOf(host, provenance, ROOTS_SETTING, targetRoot)),
|
|
192
|
+
notBuilt: read.notBuilt,
|
|
193
|
+
unanswered: read.unanswered,
|
|
162
194
|
};
|
|
163
195
|
}
|
|
164
196
|
/** The exemption classes this analyzer detects, each by its detector; any other retains nothing. */
|
|
@@ -201,10 +233,19 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
|
|
|
201
233
|
mode,
|
|
202
234
|
testFiles: new Set(resolved.testFilePaths),
|
|
203
235
|
});
|
|
236
|
+
const unanswered = [
|
|
237
|
+
...projectRead.held.unanswered,
|
|
238
|
+
...projectRead.rooted.unanswered,
|
|
239
|
+
...resolved.references
|
|
240
|
+
.filter((reference) => reference.resolution === "by-name")
|
|
241
|
+
.map((reference) => reference.to),
|
|
242
|
+
...exempt.filter(isUnansweredRecord).map((record) => record.id),
|
|
243
|
+
];
|
|
204
244
|
return {
|
|
205
245
|
configFile: projectRead.configFile,
|
|
206
246
|
configured,
|
|
207
247
|
exempt,
|
|
248
|
+
unanswered,
|
|
208
249
|
extra: extra(project, projectRead, resolved.references),
|
|
209
250
|
};
|
|
210
251
|
});
|
|
@@ -221,17 +262,64 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
|
|
|
221
262
|
return {
|
|
222
263
|
matrix: matrixOf(projects.map((one) => one.configured)),
|
|
223
264
|
exempt: exemptionsOf(projects.flatMap((one) => one.exempt)),
|
|
265
|
+
unansweredHeld: [
|
|
266
|
+
...new Set([
|
|
267
|
+
...read.answers.flatMap((one) => one.unanswered),
|
|
268
|
+
...consumed.filter((one) => one.resolution === "by-name").map((one) => one.to),
|
|
269
|
+
]),
|
|
270
|
+
],
|
|
224
271
|
projects,
|
|
225
272
|
consumers: read.consumers.loaded,
|
|
273
|
+
notBuilt: read.notBuilt,
|
|
274
|
+
unanswered: read.unanswered,
|
|
226
275
|
};
|
|
227
276
|
}
|
|
228
|
-
/**
|
|
277
|
+
/**
|
|
278
|
+
* The sweep of a run read once, under one set of marks. Where a question went unanswered,
|
|
279
|
+
* the run is swept once more without what that question could have kept live and without
|
|
280
|
+
* the references that stood in for its answer, and every declaration that sweep judges
|
|
281
|
+
* dead and this one judges live, or dead by another rule, is held by the gap too, as is
|
|
282
|
+
* every declaration that makes such a reference.
|
|
283
|
+
*/
|
|
229
284
|
function sweptOf(read, input) {
|
|
230
|
-
const swept = { ...input, exempt: read.exempt };
|
|
285
|
+
const swept = { ...input, exempt: read.exempt, unanswered: read.unansweredHeld };
|
|
286
|
+
const result = sweepMatrix(read.matrix, swept);
|
|
287
|
+
const held = new Set(read.unansweredHeld);
|
|
288
|
+
if (held.size > 0) {
|
|
289
|
+
// What a declaration reads where the checker did not answer is unknown, so a rule
|
|
290
|
+
// over what it reads, the test of dead code among them, cannot judge it.
|
|
291
|
+
for (const { configured } of read.projects) {
|
|
292
|
+
for (const reference of configured.references) {
|
|
293
|
+
if (reference.resolution === "by-name") {
|
|
294
|
+
held.add(reference.from);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const dead = new Map(result.candidates.map((candidate) => [candidate.id, candidate]));
|
|
299
|
+
const answered = matrixOf(read.projects.map(({ configured }) => ({
|
|
300
|
+
...configured,
|
|
301
|
+
references: configured.references.filter((one) => one.resolution !== "by-name"),
|
|
302
|
+
})));
|
|
303
|
+
const bare = {
|
|
304
|
+
...swept,
|
|
305
|
+
exempt: read.exempt.filter((record) => !isUnansweredRecord(record)),
|
|
306
|
+
unanswered: [],
|
|
307
|
+
};
|
|
308
|
+
for (const candidate of sweepMatrix(answered, bare).candidates) {
|
|
309
|
+
const kept = dead.get(candidate.id);
|
|
310
|
+
if (kept?.testOfDeadCode !== candidate.testOfDeadCode ||
|
|
311
|
+
kept.relation !== candidate.relation) {
|
|
312
|
+
held.add(candidate.id);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
231
316
|
return {
|
|
232
317
|
matrix: read.matrix,
|
|
233
|
-
sweep:
|
|
318
|
+
sweep: result,
|
|
234
319
|
retained: retainedIn(read.matrix, swept),
|
|
320
|
+
notBuilt: read.notBuilt,
|
|
321
|
+
unanswered: read.unanswered,
|
|
322
|
+
heldByUnanswered: [...held],
|
|
235
323
|
};
|
|
236
324
|
}
|
|
237
325
|
/**
|
|
@@ -250,8 +338,8 @@ export function runSweep(engine, host, scope, config, input, detectors = DETECTO
|
|
|
250
338
|
/** Reads a run's projects with every per-project fact an emitter reads. */
|
|
251
339
|
function readEmitterRun(engine, host, scope, config, mode, detectors) {
|
|
252
340
|
const targetRoot = scope.target.path;
|
|
253
|
-
|
|
254
|
-
|
|
341
|
+
const run = readRun(engine, host, scope, config, mode, detectors, (project, read, references) => ({
|
|
342
|
+
deprecation: deprecatedDeclarations(project, read.held),
|
|
255
343
|
accessors: accessorsOf(project, read.held),
|
|
256
344
|
needs: projectNeeds(project, read.held, host),
|
|
257
345
|
implementations: implementations(project, read.held),
|
|
@@ -261,6 +349,12 @@ function readEmitterRun(engine, host, scope, config, mode, detectors) {
|
|
|
261
349
|
declarationsNamed: declarationsNamed(project, read.held, configuredDeclarations(config)),
|
|
262
350
|
parts: projectParts(project, read.held, references, targetRoot),
|
|
263
351
|
}));
|
|
352
|
+
// Whether a declaration carries the marker decides its code, so one whose marker went
|
|
353
|
+
// unread is reported by nothing.
|
|
354
|
+
const unread = run.projects.flatMap((one) => one.extra.deprecation.unanswered);
|
|
355
|
+
return unread.length === 0
|
|
356
|
+
? run
|
|
357
|
+
: { ...run, unansweredHeld: [...new Set([...run.unansweredHeld, ...unread])] };
|
|
264
358
|
}
|
|
265
359
|
/**
|
|
266
360
|
* The input every emitter reads over one sweep of a read run: the facts beside the sweep
|
|
@@ -273,7 +367,7 @@ function emitterInputOver(host, scope, config, read, input, selfCheck) {
|
|
|
273
367
|
return {
|
|
274
368
|
config,
|
|
275
369
|
swept,
|
|
276
|
-
deprecated: new Set(projects.flatMap((one) => one.extra.deprecated)),
|
|
370
|
+
deprecated: new Set(projects.flatMap((one) => one.extra.deprecation.deprecated)),
|
|
277
371
|
stores: storesOf(projects.map((one) => ({
|
|
278
372
|
references: one.configured.references,
|
|
279
373
|
accessors: one.extra.accessors,
|
|
@@ -293,7 +387,7 @@ function emitterInputOver(host, scope, config, read, input, selfCheck) {
|
|
|
293
387
|
edges: readEdgeSides(host, targetRoot),
|
|
294
388
|
},
|
|
295
389
|
selfCheck,
|
|
296
|
-
intraFunction: intraFunctionFacts(projects.map((one) => one.extra.parts), projects.flatMap((one) => one.configured.references), new Set(read.exempt.map((one) => one.id))),
|
|
390
|
+
intraFunction: intraFunctionFacts(projects.map((one) => one.extra.parts), projects.flatMap((one) => one.configured.references), new Set([...read.exempt.map((one) => one.id), ...swept.heldByUnanswered])),
|
|
297
391
|
};
|
|
298
392
|
}
|
|
299
393
|
/**
|
|
@@ -332,6 +426,8 @@ function readAnalysis(engine, host, scope, config, provenance, mode, detectors)
|
|
|
332
426
|
})),
|
|
333
427
|
testFileRules: testFileRulesOf(config.ts.testFiles, read.projects.flatMap((one) => one.configured.testFiles)),
|
|
334
428
|
consumers: read.consumers,
|
|
429
|
+
notBuilt: read.notBuilt,
|
|
430
|
+
unanswered: read.unanswered,
|
|
335
431
|
},
|
|
336
432
|
configured: read.projects.map((one) => one.configured),
|
|
337
433
|
symbols,
|