@cplieger/deadset-ts 7.2.0-dev.4 → 8.0.0-dev.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/dist/src/analysis.js +106 -10
- package/dist/src/augmentations.js +21 -8
- package/dist/src/automatic-types.js +189 -0
- package/dist/src/base-url.js +18 -0
- package/dist/src/components.js +7 -6
- package/dist/src/config.js +1 -0
- package/dist/src/configuration-files.js +50 -12
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/convention-rows.js +75 -0
- package/dist/src/conventions.js +101 -5
- package/dist/src/decorator.js +21 -18
- package/dist/src/dependencies.js +183 -28
- package/dist/src/discover.js +132 -11
- package/dist/src/entry-point-gaps.js +7 -10
- package/dist/src/entry-points.js +112 -11
- package/dist/src/findings/completion.js +116 -21
- package/dist/src/findings/dependencies-and-module-machinery.js +71 -25
- package/dist/src/findings/emitters.js +1 -1
- package/dist/src/findings/intra-function.js +44 -13
- package/dist/src/findings/non-code-artifacts.js +66 -33
- package/dist/src/findings/unused-declarations.js +21 -1
- package/dist/src/findings/visibility-narrowing.js +22 -3
- package/dist/src/generated-file.js +11 -0
- package/dist/src/git-ignore.js +86 -0
- package/dist/src/graph.js +4 -1
- package/dist/src/import-bindings.js +97 -0
- package/dist/src/indexed-access.js +130 -28
- package/dist/src/interface-satisfaction.js +30 -12
- package/dist/src/intra-function-parts.js +297 -45
- package/dist/src/inventory.js +52 -8
- package/dist/src/kinds.js +13 -13
- package/dist/src/matrix.js +18 -9
- package/dist/src/mdx-entries.js +197 -0
- package/dist/src/module-declarations.js +9 -13
- package/dist/src/overrides.js +41 -10
- package/dist/src/print.js +1 -0
- package/dist/src/ref.js +10 -0
- package/dist/src/references.js +4 -1
- package/dist/src/report.js +4 -2
- package/dist/src/resolve.js +2 -0
- package/dist/src/roots.js +110 -38
- package/dist/src/schema.js +1 -0
- package/dist/src/serialization-contract.js +54 -8
- package/dist/src/session.js +7 -0
- package/dist/src/source-files.js +39 -14
- package/dist/src/suppress-file.js +10 -2
- package/dist/src/suppress-inline.js +18 -2
- package/dist/src/suppress.js +7 -5
- package/dist/src/sweep.js +27 -10
- package/dist/src/symbol-keys.js +103 -2
- package/dist/src/test-support.js +121 -17
- package/dist/src/type-errors.js +34 -7
- package/dist/src/type-parameter-reads.js +172 -0
- package/dist/src/verbs/analyze.js +16 -0
- package/dist/src/verbs/explain.js +4 -2
- package/dist/src/version.js +1 -1
- package/dist/src/workflow-steps.js +119 -22
- package/dist/src/workspace-programs.js +7 -3
- package/dist/src/workspace-resolution.js +53 -10
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +143 -10
- package/src/augmentations.ts +21 -10
- package/src/automatic-types.ts +246 -0
- package/src/base-url.ts +24 -0
- package/src/components.ts +7 -6
- package/src/config.ts +3 -0
- package/src/configuration-files.ts +66 -12
- package/src/conformance-record.ts +2 -2
- package/src/convention-rows.ts +103 -0
- package/src/conventions.ts +121 -7
- package/src/decorator.ts +28 -18
- package/src/dependencies.ts +247 -41
- package/src/discover.ts +156 -13
- package/src/entry-point-gaps.ts +8 -11
- package/src/entry-points.ts +133 -11
- package/src/finding.ts +10 -0
- package/src/findings/completion.ts +147 -24
- package/src/findings/dependencies-and-module-machinery.ts +90 -28
- package/src/findings/emitters.ts +1 -1
- package/src/findings/intra-function.ts +56 -14
- package/src/findings/non-code-artifacts.ts +81 -41
- package/src/findings/unused-declarations.ts +25 -1
- package/src/findings/visibility-narrowing.ts +32 -3
- package/src/generated-file.ts +16 -0
- package/src/git-ignore.ts +98 -0
- package/src/graph.ts +6 -0
- package/src/host.ts +5 -9
- package/src/import-bindings.ts +128 -0
- package/src/indexed-access.ts +152 -30
- package/src/interface-satisfaction.ts +33 -13
- package/src/intra-function-parts.ts +394 -59
- package/src/inventory.ts +71 -7
- package/src/kinds.ts +13 -13
- package/src/matrix.ts +32 -14
- package/src/mdx-entries.ts +241 -0
- package/src/module-declarations.ts +11 -13
- package/src/overrides.ts +51 -12
- package/src/print.ts +1 -0
- package/src/ref.ts +12 -0
- package/src/references.ts +10 -1
- package/src/report.ts +16 -3
- package/src/resolve.ts +3 -0
- package/src/roots.ts +163 -45
- package/src/schema.ts +1 -0
- package/src/serialization-contract.ts +55 -4
- package/src/session.ts +7 -0
- package/src/source-files.ts +66 -16
- package/src/suppress-file.ts +12 -1
- package/src/suppress-inline.ts +18 -1
- package/src/suppress.ts +9 -5
- package/src/sweep.ts +32 -11
- package/src/symbol-keys.ts +129 -2
- package/src/test-support.ts +149 -16
- package/src/type-errors.ts +38 -6
- package/src/type-parameter-reads.ts +227 -0
- package/src/verbs/analyze.ts +22 -0
- package/src/verbs/explain.ts +5 -2
- package/src/version.ts +1 -1
- package/src/workflow-steps.ts +127 -22
- package/src/workspace-programs.ts +7 -3
- package/src/workspace-resolution.ts +62 -10
package/dist/src/sweep.js
CHANGED
|
@@ -70,16 +70,30 @@ export function sweep(graph, input) {
|
|
|
70
70
|
});
|
|
71
71
|
return { liveUnder: live, candidates };
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* The directory a test runner reads a manual mock from, whose exports it puts in place of
|
|
75
|
+
* the module's: https://jestjs.io/docs/manual-mocks
|
|
76
|
+
*/
|
|
77
|
+
const MANUAL_MOCKS = "__mocks__";
|
|
78
|
+
/** Whether a test runner calls or reads one export of a test file: a function, or any export of a manual mock. */
|
|
79
|
+
export function runnerCalls(symbol) {
|
|
80
|
+
return symbol.kind === "function" || symbol.position.path.split("/").includes(MANUAL_MOCKS);
|
|
81
|
+
}
|
|
73
82
|
/**
|
|
74
83
|
* Per declaration of the graph, whether the run that counts test references reaches it.
|
|
75
|
-
* That run's roots are the graph's own and each test file,
|
|
76
|
-
*
|
|
77
|
-
*
|
|
84
|
+
* That run's roots are the graph's own and, of each test file, its top level and every
|
|
85
|
+
* function it exports, which a test runner may call, and every export of a manual mock,
|
|
86
|
+
* which the runner reads; any other export of a test file, and test-support code, which
|
|
87
|
+
* runs where a test imports it, are judged as any file's are.
|
|
88
|
+
* `held` seeds it as a mark or an exemption seeds a sweep.
|
|
78
89
|
*/
|
|
79
90
|
export function reachedWithTheTests(graph, held) {
|
|
80
91
|
const tests = graph.symbols.flatMap((symbol, at) => {
|
|
81
|
-
const
|
|
82
|
-
|
|
92
|
+
const exported = symbol.exported && graph.symbols[graph.parent[at] ?? OUTSIDE]?.kind === "file";
|
|
93
|
+
const called = exported && runnerCalls(symbol);
|
|
94
|
+
return graph.test[at] === true &&
|
|
95
|
+
graph.support[at] !== true &&
|
|
96
|
+
(symbol.kind === "file" || called)
|
|
83
97
|
? [{ at, kind: "test-runner" }]
|
|
84
98
|
: [];
|
|
85
99
|
});
|
|
@@ -162,10 +176,11 @@ function reachable(graph, held, mode) {
|
|
|
162
176
|
}
|
|
163
177
|
/**
|
|
164
178
|
* Admits every test declaration whose set of referenced production declarations is
|
|
165
|
-
* non-empty and wholly dead, and returns which it admitted.
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
179
|
+
* non-empty and wholly dead, and returns which it admitted. An ambient declaration describes
|
|
180
|
+
* code the target does not implement, so it is no production declaration here. The set is
|
|
181
|
+
* read from every reference the test makes, which a production sweep does not count. A
|
|
182
|
+
* test-support declaration a test referencing a live target also references is a live
|
|
183
|
+
* target of every test referencing it. A marked or exempt test is never admitted.
|
|
169
184
|
*/
|
|
170
185
|
function admitTestsOfDeadCode(graph, dead, held) {
|
|
171
186
|
const testCode = (at) => graph.test[at] === true || graph.support[at] === true;
|
|
@@ -174,7 +189,9 @@ function admitTestsOfDeadCode(graph, dead, held) {
|
|
|
174
189
|
let live = 0;
|
|
175
190
|
if (testCode(at) && graph.subject[at] === true && held[at] !== true) {
|
|
176
191
|
for (const edge of graph.out[at] ?? []) {
|
|
177
|
-
if (testCode(edge.to) ||
|
|
192
|
+
if (testCode(edge.to) ||
|
|
193
|
+
graph.subject[edge.to] !== true ||
|
|
194
|
+
graph.ambient[edge.to] === true) {
|
|
178
195
|
continue;
|
|
179
196
|
}
|
|
180
197
|
targets += 1;
|
package/dist/src/symbol-keys.js
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
* The members a unique symbol keys, used through element accesses. `value[key]`, where
|
|
3
3
|
* `key` is a constant of a `unique symbol` type, reads the member of the receiver's type
|
|
4
4
|
* whose computed name is that same constant, and a store through the access writes it.
|
|
5
|
-
* No name node resolves to such a member: the key's name resolves to the constant.
|
|
5
|
+
* No name node resolves to such a member: the key's name resolves to the constant. An
|
|
6
|
+
* iteration reads the member its well-known iteration symbol keys.
|
|
6
7
|
*/
|
|
7
|
-
import { isComputedPropertyName, isIdentifier, isNoSubstitutionTemplateLiteral, isNumericLiteral, isParenthesizedExpression, isPropertyAccessExpression, isStringLiteral, } from "@typescript/native/ast";
|
|
8
|
+
import { isComputedPropertyName, isForOfStatement, isIdentifier, isNoSubstitutionTemplateLiteral, isNumericLiteral, isParenthesizedExpression, isPropertyAccessExpression, isSpreadElement, isStringLiteral, } from "@typescript/native/ast";
|
|
9
|
+
import { nodeKey } from "./inventory.js";
|
|
10
|
+
import { renderPosition } from "./position.js";
|
|
8
11
|
import { isAnswered } from "./query.js";
|
|
9
12
|
/** The identifier an element access's argument names its key by, where it is a name. */
|
|
10
13
|
export function keyNameOf(argument) {
|
|
@@ -112,3 +115,101 @@ export function keyedUses(project, chains, resolved, properties, cap) {
|
|
|
112
115
|
return ids.length === 0 ? [] : [{ access: one.access, ids: [...new Set(ids)] }];
|
|
113
116
|
});
|
|
114
117
|
}
|
|
118
|
+
/** The member name `[Symbol.<name>]` spells, where a computed name spells one. */
|
|
119
|
+
function wellKnownName(name) {
|
|
120
|
+
if (name === undefined || !isComputedPropertyName(name)) {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
const key = name.expression;
|
|
124
|
+
return isPropertyAccessExpression(key) &&
|
|
125
|
+
isIdentifier(key.expression) &&
|
|
126
|
+
key.expression.text === "Symbol" &&
|
|
127
|
+
isIdentifier(key.name)
|
|
128
|
+
? key.name.text
|
|
129
|
+
: undefined;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The references the iterations of one project's own files make to the iteration members
|
|
133
|
+
* of the values they iterate: a `for…of` statement and a spread element read
|
|
134
|
+
* `[Symbol.iterator]`, and a `for await…of` statement reads `[Symbol.asyncIterator]` and,
|
|
135
|
+
* which it falls back to, `[Symbol.iterator]`. Each is made by the declaration holding it.
|
|
136
|
+
*/
|
|
137
|
+
export function iterationReferences(project, held, chains, root, testFiles) {
|
|
138
|
+
const queries = project.queries;
|
|
139
|
+
const ownPaths = project.ownPaths();
|
|
140
|
+
const iterated = [];
|
|
141
|
+
for (const file of project.ownSourceFiles()) {
|
|
142
|
+
const fileId = held.declarations.get(nodeKey(file, file));
|
|
143
|
+
if (fileId === undefined) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const visit = (node, from) => {
|
|
147
|
+
const holder = held.declarations.get(nodeKey(file, node)) ?? from;
|
|
148
|
+
if (isForOfStatement(node)) {
|
|
149
|
+
const protocols = node.awaitModifier === undefined ? ["iterator"] : ["asyncIterator", "iterator"];
|
|
150
|
+
iterated.push({ file, from: holder, operand: node.expression, protocols });
|
|
151
|
+
}
|
|
152
|
+
else if (isSpreadElement(node)) {
|
|
153
|
+
iterated.push({ file, from: holder, operand: node.expression, protocols: ["iterator"] });
|
|
154
|
+
}
|
|
155
|
+
node.forEachChild((child) => {
|
|
156
|
+
visit(child, holder);
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
file.forEachChild((child) => {
|
|
160
|
+
visit(child, fileId);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
if (iterated.length === 0) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
const types = queries.typesAt(iterated.map((one) => one.operand));
|
|
167
|
+
const members = new Map();
|
|
168
|
+
const membersOf = (type) => {
|
|
169
|
+
const known = members.get(type.id);
|
|
170
|
+
if (known !== undefined) {
|
|
171
|
+
return known;
|
|
172
|
+
}
|
|
173
|
+
const keyed = [];
|
|
174
|
+
const properties = queries.propertiesOf(type);
|
|
175
|
+
for (const property of isAnswered(properties) ? properties : []) {
|
|
176
|
+
for (const handle of property.declarations) {
|
|
177
|
+
const node = ownPaths.has(handle.path) ? project.declarationAt(handle)?.node : undefined;
|
|
178
|
+
const name = wellKnownName(node?.name);
|
|
179
|
+
if (name !== undefined) {
|
|
180
|
+
keyed.push({ name, ids: chains.declarationsOf(property) });
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
members.set(type.id, keyed);
|
|
186
|
+
return keyed;
|
|
187
|
+
};
|
|
188
|
+
const found = [];
|
|
189
|
+
iterated.forEach((one, index) => {
|
|
190
|
+
const type = types[index];
|
|
191
|
+
if (type === undefined || !isAnswered(type)) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const parts = type.isUnionType() || type.isIntersectionType() ? queries.constituents(type) : [type];
|
|
195
|
+
const position = renderPosition(one.file, root, one.operand.getStart());
|
|
196
|
+
for (const part of isAnswered(parts) ? parts : []) {
|
|
197
|
+
for (const member of membersOf(part)) {
|
|
198
|
+
if (!one.protocols.includes(member.name)) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
for (const to of member.ids) {
|
|
202
|
+
found.push({
|
|
203
|
+
from: one.from,
|
|
204
|
+
to,
|
|
205
|
+
position,
|
|
206
|
+
use: "read",
|
|
207
|
+
resolution: "keyed",
|
|
208
|
+
test: testFiles.has(position.path),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
return found;
|
|
215
|
+
}
|
package/dist/src/test-support.js
CHANGED
|
@@ -1,19 +1,116 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Test-support code: the files only tests reach
|
|
3
|
-
*
|
|
4
|
-
* included, is written in a test file or in
|
|
5
|
-
* tests: what it references is referenced by
|
|
2
|
+
* Test-support code: the files only tests reach that carry test evidence of their own. A
|
|
3
|
+
* test reaches each one through a chain of references, and every reference to a
|
|
4
|
+
* declaration of one, an import for its effects included, is written in a test file or in
|
|
5
|
+
* another such file, so it is judged with the tests: what it references is referenced by
|
|
6
|
+
* test code.
|
|
6
7
|
*/
|
|
8
|
+
import { CONVENTION_ROWS } from "./convention-rows.js";
|
|
9
|
+
import { packageOfSpecifier } from "./dependencies.js";
|
|
7
10
|
/** The root kind that never takes a file out of test-support code: the skip only withholds. */
|
|
8
11
|
const SKIP_ROOT = "type-error";
|
|
12
|
+
/**
|
|
13
|
+
* The test runners and test libraries an import of which is test evidence: each runner a
|
|
14
|
+
* convention row reads test files for, the platform's own runner, and the libraries below.
|
|
15
|
+
*/
|
|
16
|
+
const TEST_PACKAGES = new Set([
|
|
17
|
+
...CONVENTION_ROWS.filter((row) => row.testFiles !== undefined).map((row) => row.package),
|
|
18
|
+
"node:test",
|
|
19
|
+
"@playwright/test",
|
|
20
|
+
]);
|
|
21
|
+
/** The scopes every package of which is a test library. */
|
|
22
|
+
const TEST_SCOPES = ["@testing-library/"];
|
|
23
|
+
/**
|
|
24
|
+
* The Node.js built-in modules a bare specifier names, which are no package. The set is
|
|
25
|
+
* closed: Node.js adds a new built-in under the `node:` scheme alone
|
|
26
|
+
* (https://nodejs.org/api/modules.html#built-in-modules-with-mandatory-node-prefix).
|
|
27
|
+
*/
|
|
28
|
+
const NODE_BUILTINS = new Set([
|
|
29
|
+
"assert",
|
|
30
|
+
"async_hooks",
|
|
31
|
+
"buffer",
|
|
32
|
+
"child_process",
|
|
33
|
+
"cluster",
|
|
34
|
+
"console",
|
|
35
|
+
"constants",
|
|
36
|
+
"crypto",
|
|
37
|
+
"dgram",
|
|
38
|
+
"diagnostics_channel",
|
|
39
|
+
"dns",
|
|
40
|
+
"domain",
|
|
41
|
+
"events",
|
|
42
|
+
"fs",
|
|
43
|
+
"http",
|
|
44
|
+
"http2",
|
|
45
|
+
"https",
|
|
46
|
+
"inspector",
|
|
47
|
+
"module",
|
|
48
|
+
"net",
|
|
49
|
+
"os",
|
|
50
|
+
"path",
|
|
51
|
+
"perf_hooks",
|
|
52
|
+
"process",
|
|
53
|
+
"punycode",
|
|
54
|
+
"querystring",
|
|
55
|
+
"readline",
|
|
56
|
+
"repl",
|
|
57
|
+
"stream",
|
|
58
|
+
"string_decoder",
|
|
59
|
+
"sys",
|
|
60
|
+
"timers",
|
|
61
|
+
"tls",
|
|
62
|
+
"trace_events",
|
|
63
|
+
"tty",
|
|
64
|
+
"url",
|
|
65
|
+
"util",
|
|
66
|
+
"v8",
|
|
67
|
+
"vm",
|
|
68
|
+
"wasi",
|
|
69
|
+
"worker_threads",
|
|
70
|
+
"zlib",
|
|
71
|
+
]);
|
|
72
|
+
/** Whether a specifier names a test runner or a test library. */
|
|
73
|
+
function namesTestPackage(specifier) {
|
|
74
|
+
const name = specifier.startsWith("node:") ? specifier : packageOfSpecifier(specifier);
|
|
75
|
+
return (name !== undefined &&
|
|
76
|
+
(TEST_PACKAGES.has(name) || TEST_SCOPES.some((scope) => name.startsWith(scope))));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Whether a file carries test evidence: an import of a test runner or a test library, or
|
|
80
|
+
* package imports, Node built-ins aside, that are all devDependencies no file imports
|
|
81
|
+
* outside the test files and `onlyTests`, the files only tests reach.
|
|
82
|
+
*/
|
|
83
|
+
function evidenceReader(tests, evidence) {
|
|
84
|
+
const importers = new Map();
|
|
85
|
+
for (const [path, specifiers] of evidence.imports) {
|
|
86
|
+
for (const specifier of specifiers) {
|
|
87
|
+
const name = packageOfSpecifier(specifier);
|
|
88
|
+
if (name !== undefined) {
|
|
89
|
+
importers.set(name, [...(importers.get(name) ?? []), path]);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return (onlyTests, path) => {
|
|
94
|
+
const specifiers = evidence.imports.get(path) ?? [];
|
|
95
|
+
if (specifiers.some(namesTestPackage)) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
const names = specifiers.flatMap((one) => packageOfSpecifier(one) ?? []);
|
|
99
|
+
const packages = names.filter((name) => !NODE_BUILTINS.has(name));
|
|
100
|
+
return (packages.length > 0 &&
|
|
101
|
+
packages.every((name) => evidence.devDependency(path, name) &&
|
|
102
|
+
(importers.get(name) ?? []).every((one) => tests.has(one) || onlyTests.has(one))));
|
|
103
|
+
};
|
|
104
|
+
}
|
|
9
105
|
/**
|
|
10
106
|
* The files of one project that are test-support code, below the target root: the
|
|
11
107
|
* largest set of files a test file reaches, none of them a test file or holding a root,
|
|
12
|
-
* whose every user outside the set is a test
|
|
13
|
-
* reference graph: a runtime, a tool, a
|
|
14
|
-
* published API. `testFiles` are the files
|
|
108
|
+
* each carrying test evidence of its own, whose every user outside the set is a test
|
|
109
|
+
* file. A root has a caller outside the reference graph: a runtime, a tool, a
|
|
110
|
+
* configuration, or a consumer of a library's published API. `testFiles` are the files
|
|
111
|
+
* the test-file rules classified.
|
|
15
112
|
*/
|
|
16
|
-
export function testSupportFiles(symbols, references, roots, testFiles) {
|
|
113
|
+
export function testSupportFiles(symbols, references, roots, testFiles, evidence) {
|
|
17
114
|
const fileOf = new Map(symbols.map((symbol) => [symbol.id, symbol.position.path]));
|
|
18
115
|
const users = new Map();
|
|
19
116
|
const uses = new Map();
|
|
@@ -47,17 +144,24 @@ export function testSupportFiles(symbols, references, roots, testFiles) {
|
|
|
47
144
|
}
|
|
48
145
|
}
|
|
49
146
|
}
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
147
|
+
const withoutOutsideUsers = (files) => {
|
|
148
|
+
for (let changed = true; changed;) {
|
|
149
|
+
changed = false;
|
|
150
|
+
for (const path of files) {
|
|
151
|
+
const from = users.get(path) ?? new Set();
|
|
152
|
+
if (![...from].every((one) => tests.has(one) || files.has(one))) {
|
|
153
|
+
files.delete(path);
|
|
154
|
+
changed = true;
|
|
155
|
+
}
|
|
58
156
|
}
|
|
59
157
|
}
|
|
60
|
-
|
|
158
|
+
return files;
|
|
159
|
+
};
|
|
160
|
+
// Evidence reads the files only tests reach, evidence or none, so it is fixed before the
|
|
161
|
+
// files without evidence leave the set.
|
|
162
|
+
const onlyTests = withoutOutsideUsers(new Set([...reached].filter((path) => !rooted.has(path))));
|
|
163
|
+
const carries = evidenceReader(tests, evidence);
|
|
164
|
+
const support = withoutOutsideUsers(new Set([...onlyTests].filter((path) => carries(onlyTests, path))));
|
|
61
165
|
return support;
|
|
62
166
|
}
|
|
63
167
|
/** The references with every one a test-support file makes counted as a test reference. */
|
package/dist/src/type-errors.js
CHANGED
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
* which only ever withholds a finding. `projectErrors` decides, before any of these, which
|
|
6
6
|
* errors the compiler places in no file leave the project unloaded.
|
|
7
7
|
*/
|
|
8
|
-
import { isClassDeclaration, isNoSubstitutionTemplateLiteral, isPropertyAccessExpression, isStringLiteral, } from "@typescript/native/ast";
|
|
8
|
+
import { isClassDeclaration, isFunctionLikeDeclaration, isNoSubstitutionTemplateLiteral, isPropertyAccessExpression, isStringLiteral, } from "@typescript/native/ast";
|
|
9
9
|
import { packageOfSpecifier } from "./dependencies.js";
|
|
10
10
|
import { nodeKey } from "./inventory.js";
|
|
11
11
|
import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.js";
|
|
12
12
|
import { isAnswered } from "./query.js";
|
|
13
|
+
import { findWorkspace } from "./workspace.js";
|
|
13
14
|
/** The compiler's codes for an import whose module it cannot find. */
|
|
14
15
|
const MODULE_NOT_FOUND = new Set([2307, 2792]);
|
|
15
16
|
const MANIFEST = "package.json";
|
|
@@ -163,20 +164,36 @@ function expectedModule(context, importer, specifier) {
|
|
|
163
164
|
const manifest = name === undefined
|
|
164
165
|
? undefined
|
|
165
166
|
: declaringManifest(host, dirnamePath(importer), targetRoot, name);
|
|
166
|
-
|
|
167
|
+
// No run requires a nested package that is no workspace member to be installed.
|
|
168
|
+
if (manifest === undefined ||
|
|
169
|
+
(dirnamePath(manifest) !== targetRoot && !context.member(manifest))) {
|
|
167
170
|
return undefined;
|
|
168
171
|
}
|
|
169
172
|
const where = relativePath(targetRoot, manifest) ?? manifest;
|
|
170
173
|
return `a dependency ${where} declares that is not installed: run the package manager's install`;
|
|
171
174
|
}
|
|
172
175
|
/**
|
|
173
|
-
* The unit a type error at one offset skips: the
|
|
174
|
-
* top-level class does, else the top-level statement.
|
|
176
|
+
* The unit a type error at one offset skips: the innermost function holding it, else the
|
|
177
|
+
* class member holding it where a top-level class does, else the top-level statement.
|
|
175
178
|
*/
|
|
176
179
|
function unitAt(file, offset) {
|
|
177
180
|
const statement = file.statements.find((one) => one.pos <= offset && offset < one.end);
|
|
178
|
-
if (statement === undefined
|
|
179
|
-
return
|
|
181
|
+
if (statement === undefined) {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
let innermost;
|
|
185
|
+
const visit = (node) => {
|
|
186
|
+
if (offset < node.getStart() || offset >= node.end) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (isFunctionLikeDeclaration(node)) {
|
|
190
|
+
innermost = node;
|
|
191
|
+
}
|
|
192
|
+
node.forEachChild(visit);
|
|
193
|
+
};
|
|
194
|
+
visit(statement);
|
|
195
|
+
if (innermost !== undefined || !isClassDeclaration(statement)) {
|
|
196
|
+
return innermost ?? statement;
|
|
180
197
|
}
|
|
181
198
|
return statement.members.find((one) => one.pos <= offset && offset < one.end) ?? statement;
|
|
182
199
|
}
|
|
@@ -190,7 +207,17 @@ function messageOf(text, targetRoot) {
|
|
|
190
207
|
* is no error here, and one in a file that is not the project's own is that file's
|
|
191
208
|
* package's to answer for.
|
|
192
209
|
*/
|
|
193
|
-
export function readErrors(project, errors,
|
|
210
|
+
export function readErrors(project, errors, where) {
|
|
211
|
+
let workspace = null;
|
|
212
|
+
const context = {
|
|
213
|
+
...where,
|
|
214
|
+
member: (manifest) => {
|
|
215
|
+
if (workspace === null) {
|
|
216
|
+
workspace = findWorkspace(where.host, where.targetRoot, () => undefined);
|
|
217
|
+
}
|
|
218
|
+
return workspace?.memberOf(manifest) !== undefined;
|
|
219
|
+
},
|
|
220
|
+
};
|
|
194
221
|
const files = new Map(project.ownSourceFiles().map((file) => [file.fileName, file]));
|
|
195
222
|
const missing = [];
|
|
196
223
|
const skips = [];
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The members a read through a value typed by a type parameter reaches: `value.kind`,
|
|
3
|
+
* where `value` is of a type parameter `T` of a generic function, reads `kind` on each
|
|
4
|
+
* object type no class declares that a call of the function passes for `T`, because the
|
|
5
|
+
* call's value is what the read reads, whatever member of the constraint the name resolves
|
|
6
|
+
* to. A class's members a constraint requires are interface satisfaction's.
|
|
7
|
+
*/
|
|
8
|
+
import { isBinaryExpression, isCallExpression, isIdentifier, isNewExpression, isPropertyAccessExpression, isTypeParameterDeclaration, SyntaxKind, } from "@typescript/native/ast";
|
|
9
|
+
import { SymbolFlags } from "@typescript/native/sync";
|
|
10
|
+
import { nodeKey } from "./inventory.js";
|
|
11
|
+
import { renderPosition } from "./position.js";
|
|
12
|
+
import { isAnswered } from "./query.js";
|
|
13
|
+
/** How deep a parameter's type is matched against the type a call passes: `readonly T[]` is one level. */
|
|
14
|
+
const MATCH_DEPTH = 3;
|
|
15
|
+
/** The name a call reaches one generic by: its own, or that of what holds it. */
|
|
16
|
+
function nameOf(generic) {
|
|
17
|
+
const own = generic.name;
|
|
18
|
+
const holder = generic.parent?.name;
|
|
19
|
+
const name = own ?? holder;
|
|
20
|
+
return name !== undefined && isIdentifier(name) ? name.text : undefined;
|
|
21
|
+
}
|
|
22
|
+
/** Whether a property access is the target of a plain assignment, which writes the member. */
|
|
23
|
+
function assignedTo(access) {
|
|
24
|
+
const parent = access.parent;
|
|
25
|
+
return (isBinaryExpression(parent) &&
|
|
26
|
+
parent.operatorToken.kind === SyntaxKind.EqualsToken &&
|
|
27
|
+
parent.left === access);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The references the reads through type-parameter-typed values of one project's own files
|
|
31
|
+
* make to the members of the types its calls pass, each made by the declaration holding
|
|
32
|
+
* the read.
|
|
33
|
+
*/
|
|
34
|
+
export function typeParameterReads(project, held, chains, root, testFiles) {
|
|
35
|
+
const queries = project.queries;
|
|
36
|
+
const reads = [];
|
|
37
|
+
const calls = [];
|
|
38
|
+
for (const file of project.ownSourceFiles()) {
|
|
39
|
+
const fileId = held.declarations.get(nodeKey(file, file));
|
|
40
|
+
if (fileId === undefined) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const visit = (node, from, generic) => {
|
|
44
|
+
const holder = held.declarations.get(nodeKey(file, node)) ?? from;
|
|
45
|
+
const inside = generic ||
|
|
46
|
+
node.typeParameters !== undefined;
|
|
47
|
+
if (inside &&
|
|
48
|
+
isPropertyAccessExpression(node) &&
|
|
49
|
+
isIdentifier(node.name) &&
|
|
50
|
+
!assignedTo(node)) {
|
|
51
|
+
reads.push({ file, from: holder, access: node });
|
|
52
|
+
}
|
|
53
|
+
if (isCallExpression(node) || isNewExpression(node)) {
|
|
54
|
+
calls.push(node);
|
|
55
|
+
}
|
|
56
|
+
node.forEachChild((child) => {
|
|
57
|
+
visit(child, holder, inside);
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
file.forEachChild((child) => {
|
|
61
|
+
visit(child, fileId, false);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (reads.length === 0) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
// The reads whose receiver the checker types as a type parameter, by the generic declaring it.
|
|
68
|
+
const receivers = queries.typesAt(reads.map((read) => read.access.expression));
|
|
69
|
+
const byGeneric = new Map();
|
|
70
|
+
reads.forEach((read, at) => {
|
|
71
|
+
const type = receivers[at];
|
|
72
|
+
if (type === undefined || !isAnswered(type) || !type.isTypeParameter()) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const symbol = queries.symbolOfType(type);
|
|
76
|
+
const declaration = symbol === undefined || !isAnswered(symbol)
|
|
77
|
+
? undefined
|
|
78
|
+
: symbol.declarations
|
|
79
|
+
.map((handle) => project.declarationAt(handle)?.node)
|
|
80
|
+
.find((node) => node !== undefined && isTypeParameterDeclaration(node));
|
|
81
|
+
const generic = declaration?.parent;
|
|
82
|
+
if (generic !== undefined) {
|
|
83
|
+
byGeneric.set(generic, [...(byGeneric.get(generic) ?? []), { read, parameter: type }]);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
if (byGeneric.size === 0) {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
/** The types `instantiated` holds where `declared` holds `parameter`, matched part for part. */
|
|
90
|
+
const passedFor = (parameter, declared, instantiated, depth) => {
|
|
91
|
+
if (declared.id === parameter.id) {
|
|
92
|
+
return [instantiated];
|
|
93
|
+
}
|
|
94
|
+
if (depth >= MATCH_DEPTH) {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
const a = queries.typeArguments(declared);
|
|
98
|
+
const b = queries.typeArguments(instantiated);
|
|
99
|
+
if (!isAnswered(a) || !isAnswered(b) || a.length !== b.length) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
return a.flatMap((one, at) => {
|
|
103
|
+
const other = b[at];
|
|
104
|
+
return other === undefined ? [] : passedFor(parameter, one, other, depth + 1);
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
// Only a call spelling a generic's name is resolved: one request each.
|
|
108
|
+
const spelled = new Set([...byGeneric.keys()].flatMap((generic) => nameOf(generic) ?? []));
|
|
109
|
+
const found = new Map();
|
|
110
|
+
for (const call of calls) {
|
|
111
|
+
const callee = call.expression;
|
|
112
|
+
const written = isIdentifier(callee)
|
|
113
|
+
? callee.text
|
|
114
|
+
: isPropertyAccessExpression(callee) && isIdentifier(callee.name)
|
|
115
|
+
? callee.name.text
|
|
116
|
+
: undefined;
|
|
117
|
+
if (written === undefined || !spelled.has(written)) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const resolved = queries.resolvedSignature(call);
|
|
121
|
+
if (resolved === undefined || !isAnswered(resolved) || resolved.declaration === undefined) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const generic = project.declarationAt(resolved.declaration)?.node;
|
|
125
|
+
const pending = generic === undefined ? undefined : byGeneric.get(generic);
|
|
126
|
+
const declared = pending === undefined ? undefined : queries.genericOf(resolved);
|
|
127
|
+
if (pending === undefined || declared === undefined || !isAnswered(declared)) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const count = (generic?.parameters ?? []).length;
|
|
131
|
+
for (const { read, parameter } of pending) {
|
|
132
|
+
const passed = [];
|
|
133
|
+
for (let at = 0; at < count; at += 1) {
|
|
134
|
+
const own = queries.parameterType(declared, at);
|
|
135
|
+
const given = queries.typeAtPosition(resolved, at);
|
|
136
|
+
if (own !== undefined && isAnswered(own) && isAnswered(given)) {
|
|
137
|
+
passed.push(...passedFor(parameter, own, given, 0));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const name = read.access.name.text;
|
|
141
|
+
const position = renderPosition(read.file, root, read.access.name.getStart());
|
|
142
|
+
for (const type of passed) {
|
|
143
|
+
const parts = type.isUnionType() ? queries.constituents(type) : [type];
|
|
144
|
+
for (const part of isAnswered(parts) ? parts : []) {
|
|
145
|
+
const owner = queries.symbolOfType(part);
|
|
146
|
+
if (!isAnswered(owner) ||
|
|
147
|
+
owner === undefined ||
|
|
148
|
+
(owner.flags & SymbolFlags.Class) !== 0) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const properties = queries.propertiesOf(part);
|
|
152
|
+
for (const property of isAnswered(properties) ? properties : []) {
|
|
153
|
+
if (property.name !== name) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
for (const to of chains.declarationsOf(property)) {
|
|
157
|
+
found.set(`${read.from}\u0000${to}\u0000${String(read.access.name.pos)}`, {
|
|
158
|
+
from: read.from,
|
|
159
|
+
to,
|
|
160
|
+
position,
|
|
161
|
+
use: "read",
|
|
162
|
+
resolution: "batch",
|
|
163
|
+
test: testFiles.has(position.path),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return [...found.values()];
|
|
172
|
+
}
|
|
@@ -62,6 +62,21 @@ function runRelative(invoked, path) {
|
|
|
62
62
|
}
|
|
63
63
|
return below;
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* One note per manifest entry the run reads back to no source file: what the entry names
|
|
67
|
+
* reaches nothing, so the findings its file would have rooted are reported.
|
|
68
|
+
*/
|
|
69
|
+
function unmappedNotes(entries, targetRoot) {
|
|
70
|
+
return entries.map((entry) => {
|
|
71
|
+
const named = relativePath(targetRoot, entry.path) ?? entry.path;
|
|
72
|
+
return {
|
|
73
|
+
kind: "unmapped-entry",
|
|
74
|
+
path: relativePath(targetRoot, entry.dir) ?? ".",
|
|
75
|
+
key: "ts.entry_files",
|
|
76
|
+
message: `the manifest's ${entry.member} names ${named}, which reads back to no source file of the target: name the file it is built from in ts.entry_files`,
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
}
|
|
65
80
|
/** Each project the run dropped as a report names it, by its configuration file below the target. */
|
|
66
81
|
function notBuiltOf(dropped, targetRoot) {
|
|
67
82
|
return dropped.map((one) => {
|
|
@@ -207,6 +222,7 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
|
|
|
207
222
|
typeErrorSkips: run.typeErrorSkips,
|
|
208
223
|
unanswered: unansweredCounts(analysis),
|
|
209
224
|
conventionsApplied: run.conventionsApplied,
|
|
225
|
+
notes: unmappedNotes(run.unmappedEntries, resolvePath(invoked, targetRoot)),
|
|
210
226
|
});
|
|
211
227
|
report = capFindings(sortFindings(built, config.reporters.sort), config.reporters.maxFindings);
|
|
212
228
|
verdict = verdictOf(result, config);
|
|
@@ -257,11 +257,13 @@ function reported(found) {
|
|
|
257
257
|
/**
|
|
258
258
|
* Every finding of the run that names the declaration by its reference and reports at
|
|
259
259
|
* another position: a part of it, or a suppression record naming it. Each is published under
|
|
260
|
-
* the reference asked about,
|
|
260
|
+
* the reference asked about, or a parameter's under one it starts, so an explanation leaving
|
|
261
|
+
* it out would disagree with the report.
|
|
261
262
|
*/
|
|
262
263
|
function namedElsewhere(findings, subject) {
|
|
263
264
|
return findings
|
|
264
|
-
.filter((found) => found.symbol.ref === subject.ref &&
|
|
265
|
+
.filter((found) => (found.symbol.holder ?? found.symbol.ref) === subject.ref &&
|
|
266
|
+
positionKey(found.position) !== subject.id)
|
|
265
267
|
.map((found) => `also reported: ${found.code} ${found.kind}\t${positionKey(found.position)}\t${found.symbol.kind} ${found.symbol.name}\n`)
|
|
266
268
|
.join("");
|
|
267
269
|
}
|
package/dist/src/version.js
CHANGED