@cplieger/deadset-ts 3.13.0 → 4.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/README.md +3 -3
- package/dist/bin/node-host.js +10 -7
- package/dist/src/alias-chain.js +59 -7
- package/dist/src/analysis.js +119 -23
- 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 +105 -28
- 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 +48 -7
- package/dist/src/reporters/text.js +8 -6
- package/dist/src/roots.js +27 -11
- 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 +40 -7
- 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 +223 -61
- 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 +177 -33
- 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 +50 -8
- package/src/reporters/text.ts +12 -12
- package/src/roots.ts +34 -12
- 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 +53 -7
- package/src/verbs/print-retained.ts +6 -2
- package/src/verbs/print-roots.ts +5 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ deadset-ts is a command-line analyzer for TypeScript and JavaScript projects. It
|
|
|
8
8
|
|
|
9
9
|
deadset-ts is report-only. It never edits source, and it refuses any `--fix` flag.
|
|
10
10
|
|
|
11
|
-
**Status: pre-release.** This version implements contract 4.0.0 and ships the analyzer's foundation: one compiler session per run over the projects `analysis.configurations` declares, in the order it lists them, each a compiler configuration file below the target, or, where it declares none, every `tsconfig` project discovered under the target (the explicit scope, every `tsconfig*.json`, each project's `references`), a fail-closed read of every project's diagnostics, configuration decoding against the contract's closed key list with `print-config` and its provenance, `print-projects`, the report-only guard, the symbol inventory of every declaration a project's own files hold down to its class and type members, the one-pass reference resolution over that inventory, which resolves each file's identifiers in batches ([batch-cap calibration](docs/batch-cap-calibration.md) is the measurement behind the batch size), and the root set `print-roots` prints. A root is a declaration the analysis keeps live without a reference: what a file exports when the manifest names it through `main`, `module`, `types`, `bin` or `exports`, or when `ts.entry_files` matches it; a library target's published API; the configuration, setup and test files of Vitest, Stryker and Playwright, the flat configuration of ESLint, and a worker or service worker a call addresses by a string literal; and every declaration a `roots.patterns` entry names. An entry that names nothing is reported as `DS1704` and fails the run. The entry-point conventions of other tools are not read, and `DECLINED_CONVENTIONS` lists each one with the reason. Seven exemption classes hold a declaration back from the sweep, and `print-retained` lists what each one held back: `interface-satisfaction`, a member an interface requires of a class whose value reaches that interface; `decorator`, a decorated member and every member of a decorated class; `injection-container`, a decorated property of a class a dependency-injection container constructs, being a class passed to a call `ts.injection_registrations` names or one a parameter of whose constructor carries a decorator; `framework-lifecycle`, a member a `ts.lifecycle_contracts` entry lists, on a class that is the entry's component by a decorator or a call the entry names or by extending a class it names; `serialization-contract`, the properties of a class whose values reach `JSON.stringify` or a declaration `ts.serializers` names, and those and its `toJSON` and `toString` where a value is passed to an `unknown` or `any` parameter of a function outside the analysis; and, at the lowest confidence, `template-field`, a member an action of a template under `analysis.template_dirs` names, and `reflective-lookup`, a member a literal key of an element access or of `Reflect.get`, `Reflect.set` or `Reflect.has` names. None of them holds back a `#private` member. `analyze` writes the report the Contract's report schema declares, with text, JSON, GitHub annotation, SARIF 2.1.0 and template renderings beside it on request. A scope document passed with `--scope` names the consumers loaded beside the target, each a directory whose compiler configurations open in the same session; a reference from a consumer keeps a target declaration live, the report names every consumer it loaded, and a consumer that is absent or does not load ends the run with exit code 3. A library target's published API is `certain` once every declared consumer loads and `possible` with no consumer information, and in that second case `DS1001` defaults to `allow` and the narrowing kinds report only in files no manifest export reaches. `explain` answers why one declaration is reported, retained, live or dead, from the same analysis the report is built from. `describe` prints the analyzer's description with its conformance record over the published corpus and the declared gaps `conformance.json` lists. The analysis is reached through the command, and through `run()` for a caller that runs the command in-process. The one runtime dependency is the `typescript` package at exactly 7.0.
|
|
11
|
+
**Status: pre-release.** This version implements contract 4.0.0 and ships the analyzer's foundation: one compiler session per run over the projects `analysis.configurations` declares, in the order it lists them, each a compiler configuration file below the target, or, where it declares none, every `tsconfig` project discovered under the target (the explicit scope, every `tsconfig*.json`, each project's `references`), a fail-closed read of every project's diagnostics, where a discovered project whose configuration names no input, carries an option the compiler refuses or cannot be read is dropped, named in the report's `configurations_not_built` with its error and on standard error, and the rest analyzed, while a project `analysis.configurations` or the scope names ends the run with exit code 3, configuration decoding against the contract's closed key list with `print-config` and its provenance, `print-projects`, the report-only guard, the symbol inventory of every declaration a project's own files hold down to its class and type members, the one-pass reference resolution over that inventory, which resolves each file's identifiers in batches ([batch-cap calibration](docs/batch-cap-calibration.md) is the measurement behind the batch size), and the root set `print-roots` prints. A root is a declaration the analysis keeps live without a reference: what a file exports when the manifest names it through `main`, `module`, `types`, `bin` or `exports`, or when `ts.entry_files` matches it; a library target's published API; the configuration, setup and test files of Vitest, Stryker and Playwright, the flat configuration of ESLint, and a worker or service worker a call addresses by a string literal; and every declaration a `roots.patterns` entry names. An entry that names nothing is reported as `DS1704` and fails the run. The entry-point conventions of other tools are not read, and `DECLINED_CONVENTIONS` lists each one with the reason. Seven exemption classes hold a declaration back from the sweep, and `print-retained` lists what each one held back: `interface-satisfaction`, a member an interface requires of a class whose value reaches that interface; `decorator`, a decorated member and every member of a decorated class; `injection-container`, a decorated property of a class a dependency-injection container constructs, being a class passed to a call `ts.injection_registrations` names or one a parameter of whose constructor carries a decorator; `framework-lifecycle`, a member a `ts.lifecycle_contracts` entry lists, on a class that is the entry's component by a decorator or a call the entry names or by extending a class it names; `serialization-contract`, the properties of a class whose values reach `JSON.stringify` or a declaration `ts.serializers` names, and those and its `toJSON` and `toString` where a value is passed to an `unknown` or `any` parameter of a function outside the analysis; and, at the lowest confidence, `template-field`, a member an action of a template under `analysis.template_dirs` names, and `reflective-lookup`, a member a literal key of an element access or of `Reflect.get`, `Reflect.set` or `Reflect.has` names. None of them holds back a `#private` member. `analyze` writes the report the Contract's report schema declares, with text, JSON, GitHub annotation, SARIF 2.1.0 and template renderings beside it on request. A scope document passed with `--scope` names the consumers loaded beside the target, each a directory whose compiler configurations open in the same session; a reference from a consumer keeps a target declaration live, the report names every consumer it loaded, and a consumer that is absent or does not load ends the run with exit code 3. A library target's published API is `certain` once every declared consumer loads and `possible` with no consumer information, and in that second case `DS1001` defaults to `allow` and the narrowing kinds report only in files no manifest export reaches. `explain` answers why one declaration is reported, retained, held by an unanswered question, live or dead, from the same analysis the report is built from. `describe` prints the analyzer's description with its conformance record over the published corpus and the declared gaps `conformance.json` lists. The analysis is reached through the command, and through `run()` for a caller that runs the command in-process. Every question the analysis asks the compiler is guarded: a question the compiler server fails on is answered as unknown, every declaration that answer could have kept live is kept live and reported by nothing, as is every declaration only those keep live or whose own references went unanswered, and standard error counts the unanswered questions by project. The one runtime dependency is the `typescript` package at exactly 7.1.0-dev.20261003.1, installed under the `@typescript/native` name.
|
|
12
12
|
|
|
13
13
|
## Install
|
|
14
14
|
|
|
@@ -28,7 +28,7 @@ The npm package carries the command and the library as JavaScript that the compi
|
|
|
28
28
|
npx deadset-ts version
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
`version` prints the analyzer version and the Contract version it implements, and exits 0. `print-config`, `print-projects`, `print-roots` and `print-retained` print what their row of the table says and exit 0, or 1 when `print-roots` reports a `DS1704`, and `print-retained` exits 2 when an `exemptions.disabled` entry is not an exemption class or an `analysis.template_dirs` entry is not a directory below the target root. `analyze --report=PATH` writes the JSON report to `PATH` through a temporary file renamed into place, each `--format` rendering at `PATH` with its suffix appended (`text` as `.txt`, `json` as `.json`, `github` as `.annotations` in GitHub workflow-command form, `sarif` as `.sarif`, and `template` as `.tmpl`, rendering the file `--template=FILE` names, written in a subset of Go's `text/template` action grammar over the report's JSON member names), and with `--baseline-write=FILE` a baseline of every finding; it writes nothing to standard output and exits with the Contract's exit-code table: 0 clean, 1 for a finding at or above `reporters.fail_on` or a stale suppression, 2 for a usage error, including a template that cannot be read or does not parse, 3 for a failed load or a rendering that cannot be written, and 4 for a pending finding, which outranks 1. `--exit-code=off` exits 0 whatever the verdict and names the verdict on standard error. The report names the target relative to the directory the run is invoked from, so a target outside that directory fails with 3. `describe` writes its JSON document to standard output and exits 0. `explain --why=SYM`, or the same request spelled `--why-live=SYM` or `--why-not=SYM`, prints why that one declaration is reported, retained, live or dead, with the shortest reference path from a root or a loaded consumer for a live one, and exits 0; a request that names no one declaration exits 2 and lists up to 20 declarations it partially matches. Any other invocation and no arguments print the usage line and exit 2.
|
|
31
|
+
`version` prints the analyzer version and the Contract version it implements, and exits 0. `print-config`, `print-projects`, `print-roots` and `print-retained` print what their row of the table says and exit 0, or 1 when `print-roots` reports a `DS1704`, and `print-retained` exits 2 when an `exemptions.disabled` entry is not an exemption class or an `analysis.template_dirs` entry is not a directory below the target root. `analyze --report=PATH` writes the JSON report to `PATH` through a temporary file renamed into place, each `--format` rendering at `PATH` with its suffix appended (`text` as `.txt`, `json` as `.json`, `github` as `.annotations` in GitHub workflow-command form, `sarif` as `.sarif`, and `template` as `.tmpl`, rendering the file `--template=FILE` names, written in a subset of Go's `text/template` action grammar over the report's JSON member names), and with `--baseline-write=FILE` a baseline of every finding; it writes nothing to standard output and exits with the Contract's exit-code table: 0 clean, 1 for a finding at or above `reporters.fail_on` or a stale suppression, 2 for a usage error, including a template that cannot be read or does not parse, 3 for a failed load or a rendering that cannot be written, and 4 for a pending finding, which outranks 1. `--exit-code=off` exits 0 whatever the verdict and names the verdict on standard error. The report names the target relative to the directory the run is invoked from, so a target outside that directory fails with 3. `describe` writes its JSON document to standard output and exits 0. `explain --why=SYM`, or the same request spelled `--why-live=SYM` or `--why-not=SYM`, prints why that one declaration is reported, retained, held by an unanswered question, live or dead, with the shortest reference path from a root or a loaded consumer for a live one, and exits 0; a request that names no one declaration exits 2 and lists up to 20 declarations it partially matches. Any other invocation and no arguments print the usage line and exit 2.
|
|
32
32
|
|
|
33
33
|
| Verb | Job |
|
|
34
34
|
| ---------------- | ---------------------------------------------------------------------------------- |
|
|
@@ -44,7 +44,7 @@ npx deadset-ts version
|
|
|
44
44
|
## Requirements
|
|
45
45
|
|
|
46
46
|
- Node.js 24 or later. The npm command is JavaScript the compiler emits at publish, so it runs with no loader and no flag; a clone of this repository runs the same command from its TypeScript sources through Node's own type stripping, which is what needs the version.
|
|
47
|
-
- TypeScript 7. The analyzer is written against the `typescript` package at exactly version 7.0.
|
|
47
|
+
- TypeScript 7. The analyzer is written against the `typescript` package at exactly version 7.1.0-dev.20261003.1 and its `unstable/*` API. It does not run on TypeScript 6, whose compiler API TypeScript 7 removed, and it does not fall back to it.
|
|
48
48
|
- Linux. Other platforms are untested.
|
|
49
49
|
|
|
50
50
|
## API
|
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
132
|
throw new DiscoveryError(`${String(failures.length)} error(s); no answer was produced`, failures);
|
|
111
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}`, []);
|
|
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,
|
package/dist/src/calls.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { isArrayLiteralExpression, isCallExpression, isClassDeclaration, isDecorator, isIdentifier, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadElement, } from "@typescript/native/unstable/ast";
|
|
9
9
|
import { SymbolFlags } from "@typescript/native/unstable/sync";
|
|
10
10
|
import { nodeKey } from "./inventory.js";
|
|
11
|
-
import {
|
|
11
|
+
import { must } from "./query.js";
|
|
12
12
|
/** The name node a callee or a decorator expression resolves through, where it has one. */
|
|
13
13
|
export function calleeName(expression) {
|
|
14
14
|
if (isParenthesizedExpression(expression)) {
|
|
@@ -37,31 +37,32 @@ export function decoratorsOf(node) {
|
|
|
37
37
|
/**
|
|
38
38
|
* What each of a batch of nodes resolves to, every alias followed to the declaration
|
|
39
39
|
* it stands for. A node the checker resolves to nothing, and an alias that resolves
|
|
40
|
-
* to nothing, are absent from the answer. Each alias is followed once.
|
|
40
|
+
* to nothing, are absent from the answer. Each alias is followed once. A question the
|
|
41
|
+
* checker does not answer stops the pass, as {@link must} stops.
|
|
41
42
|
*/
|
|
42
43
|
export function resolvedTargets(project, nodes) {
|
|
43
44
|
const followed = new Map();
|
|
44
45
|
const targets = new Map();
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
target = followed.get(symbol.id);
|
|
59
|
-
}
|
|
60
|
-
if (target !== undefined) {
|
|
61
|
-
targets.set(node, target);
|
|
46
|
+
project
|
|
47
|
+
.symbolsAt(nodes.map((node) => project.handle(node)))
|
|
48
|
+
.map(must)
|
|
49
|
+
.forEach((symbol, index) => {
|
|
50
|
+
const node = nodes[index];
|
|
51
|
+
if (symbol === undefined || node === undefined) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
let target = symbol;
|
|
55
|
+
if ((symbol.flags & SymbolFlags.Alias) !== 0) {
|
|
56
|
+
if (!followed.has(symbol.id)) {
|
|
57
|
+
const aliased = must(project.queries.aliased(symbol));
|
|
58
|
+
followed.set(symbol.id, aliased.declarations.length === 0 ? undefined : aliased);
|
|
62
59
|
}
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
target = followed.get(symbol.id);
|
|
61
|
+
}
|
|
62
|
+
if (target !== undefined) {
|
|
63
|
+
targets.set(node, target);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
65
66
|
return targets;
|
|
66
67
|
}
|
|
67
68
|
/** The values one expression passes: itself, or what an array or object literal holds. */
|
|
@@ -109,7 +110,7 @@ export function classesPassed(project, held, chains, calls) {
|
|
|
109
110
|
const classes = new Set(held.symbols.filter((symbol) => symbol.kind === "class").map((symbol) => symbol.id));
|
|
110
111
|
const classesOf = (value) => {
|
|
111
112
|
const symbol = value.shorthand
|
|
112
|
-
? project.shorthandValueAt(project.handle(value.name))
|
|
113
|
+
? must(project.shorthandValueAt(project.handle(value.name)))
|
|
113
114
|
: targets.get(value.name);
|
|
114
115
|
return symbol === undefined
|
|
115
116
|
? []
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { isClassDeclaration, isClassExpression } from "@typescript/native/unstable/ast";
|
|
7
7
|
import { nodeKey } from "./inventory.js";
|
|
8
|
-
import {
|
|
8
|
+
import { must } from "./query.js";
|
|
9
9
|
/** The name node of each class declaration of the target, by the class's identifier. */
|
|
10
10
|
export function classNames(project, held) {
|
|
11
11
|
const names = new Map();
|
|
@@ -23,13 +23,12 @@ export function classNames(project, held) {
|
|
|
23
23
|
}
|
|
24
24
|
return names;
|
|
25
25
|
}
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* The types of a batch of nodes, for a pass that cannot go on past a type the checker
|
|
28
|
+
* did not answer: it stops there, as {@link must} stops.
|
|
29
|
+
*/
|
|
27
30
|
export function typesAt(project, nodes) {
|
|
28
|
-
|
|
29
|
-
for (let from = 0; from < nodes.length; from += DEFAULT_BATCH_CAP) {
|
|
30
|
-
types.push(...project.checker.getTypeAtLocation(nodes.slice(from, from + DEFAULT_BATCH_CAP)));
|
|
31
|
-
}
|
|
32
|
-
return types;
|
|
31
|
+
return project.queries.typesAt(nodes).map(must);
|
|
33
32
|
}
|
|
34
33
|
/**
|
|
35
34
|
* The type one side of each class is: the instance type at the class's name, or the
|
|
@@ -39,9 +38,9 @@ function sideTypes(project, nodes, side) {
|
|
|
39
38
|
if (side === "instance") {
|
|
40
39
|
return typesAt(project, nodes);
|
|
41
40
|
}
|
|
42
|
-
const symbols = project.symbolsAt(nodes.map((node) => project.handle(node)));
|
|
41
|
+
const symbols = project.symbolsAt(nodes.map((node) => project.handle(node))).map(must);
|
|
43
42
|
const present = symbols.filter((symbol) => symbol !== undefined);
|
|
44
|
-
const types =
|
|
43
|
+
const types = project.queries.typesOfSymbols(present).map(must);
|
|
45
44
|
let next = 0;
|
|
46
45
|
return symbols.map((symbol) => (symbol === undefined ? undefined : types[next++]));
|
|
47
46
|
}
|
|
@@ -52,7 +51,7 @@ function sideTypes(project, nodes, side) {
|
|
|
52
51
|
*/
|
|
53
52
|
export function classMembers(project, held, classIds, side) {
|
|
54
53
|
const named = [...classNames(project, held)].filter(([id]) => classIds.has(id));
|
|
55
|
-
const own =
|
|
54
|
+
const own = project.ownPaths();
|
|
56
55
|
const types = sideTypes(project, named.map(([, node]) => node), side);
|
|
57
56
|
const members = new Map();
|
|
58
57
|
named.forEach(([id], index) => {
|
|
@@ -61,7 +60,7 @@ export function classMembers(project, held, classIds, side) {
|
|
|
61
60
|
return;
|
|
62
61
|
}
|
|
63
62
|
const found = [];
|
|
64
|
-
for (const property of project.
|
|
63
|
+
for (const property of must(project.queries.propertiesOf(type))) {
|
|
65
64
|
for (const handle of property.declarations) {
|
|
66
65
|
if (!own.has(handle.path)) {
|
|
67
66
|
continue;
|