@cplieger/deadset-ts 7.2.0-dev.4 → 8.0.0-dev.2
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 +108 -10
- package/dist/src/augmentations.js +62 -18
- 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 +98 -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 +172 -40
- 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 +30 -5
- package/dist/src/findings/visibility-narrowing.js +29 -5
- 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 +57 -10
- 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 +38 -4
- 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 +8 -5
- package/dist/src/sweep.js +27 -10
- package/dist/src/symbol-keys.js +103 -2
- package/dist/src/test-support.js +119 -18
- 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 +146 -11
- package/src/augmentations.ts +76 -22
- 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 +128 -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 +15 -0
- package/src/findings/completion.ts +222 -46
- 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 +36 -5
- package/src/findings/visibility-narrowing.ts +40 -5
- 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 +104 -15
- 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 +48 -3
- 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 +10 -5
- package/src/sweep.ts +32 -11
- package/src/symbol-keys.ts +129 -2
- package/src/test-support.ts +155 -18
- 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/src/suppress-inline.ts
CHANGED
|
@@ -10,8 +10,10 @@ import {
|
|
|
10
10
|
type Node,
|
|
11
11
|
type SourceFile,
|
|
12
12
|
} from "@typescript/native/ast";
|
|
13
|
+
import { importBindings } from "./import-bindings.ts";
|
|
13
14
|
import { isParameterProperty, nodeKey, type Inventory } from "./inventory.ts";
|
|
14
15
|
import { byPosition, positionKey, renderPosition, type Position } from "./position.ts";
|
|
16
|
+
import { importRef } from "./ref.ts";
|
|
15
17
|
import type { ProjectView } from "./session.ts";
|
|
16
18
|
import { signatureOf } from "./signatures.ts";
|
|
17
19
|
import {
|
|
@@ -200,6 +202,7 @@ function declarationLines(
|
|
|
200
202
|
file: SourceFile,
|
|
201
203
|
held: Inventory,
|
|
202
204
|
symbols: ReadonlyMap<string, Declared>,
|
|
205
|
+
targetRoot: string,
|
|
203
206
|
): ReadonlyMap<number, readonly Binding[]> {
|
|
204
207
|
const below = new Map<number, Binding[]>();
|
|
205
208
|
const add = (offset: number, one: Binding): void => {
|
|
@@ -245,6 +248,20 @@ function declarationLines(
|
|
|
245
248
|
node.forEachChild(visit);
|
|
246
249
|
};
|
|
247
250
|
file.forEachChild(visit);
|
|
251
|
+
// An import binding is a subject of its own, bound on its line as a declaration is.
|
|
252
|
+
const fileId = held.declarations.get(nodeKey(file, file));
|
|
253
|
+
const moduleRef = held.symbols.find((symbol) => symbol.id === fileId)?.ref ?? "";
|
|
254
|
+
for (const name of moduleRef === "" ? [] : importBindings(file)) {
|
|
255
|
+
const position = renderPosition(file, targetRoot, name.getStart());
|
|
256
|
+
const one = { id: positionKey(position), ref: importRef(moduleRef, name.text), position };
|
|
257
|
+
let statement: Node = name;
|
|
258
|
+
while (statement.parent.kind !== SyntaxKind.SourceFile) {
|
|
259
|
+
statement = statement.parent;
|
|
260
|
+
}
|
|
261
|
+
for (const at of [statement.pos, name.getStart()]) {
|
|
262
|
+
add(at, { declared: one, part: one });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
248
265
|
return below;
|
|
249
266
|
}
|
|
250
267
|
|
|
@@ -285,7 +302,7 @@ export function inlineDirectives<Brand>(
|
|
|
285
302
|
if (comments.length === 0) {
|
|
286
303
|
continue;
|
|
287
304
|
}
|
|
288
|
-
const below = declarationLines(file, held, symbols);
|
|
305
|
+
const below = declarationLines(file, held, symbols, targetRoot);
|
|
289
306
|
for (const comment of comments) {
|
|
290
307
|
const site = renderPosition(file, targetRoot, comment.offset);
|
|
291
308
|
const target = trails(file, comment.offset) ? site.line : site.line + 1;
|
package/src/suppress.ts
CHANGED
|
@@ -115,6 +115,7 @@ export const GO_REF_EXPRESSIONS: readonly string[] = [
|
|
|
115
115
|
`^go://${GO_PATH}#${GO_IDENT}$`,
|
|
116
116
|
`^go://${GO_PATH}#${GO_IDENT}(?:\\.${GO_IDENT})+$`,
|
|
117
117
|
`^go://${GO_PATH}#${GO_IDENT}(?:\\.${GO_IDENT})?\\[${GO_IDENT}\\]$`,
|
|
118
|
+
`^go://${GO_PATH}#(?:${GO_IDENT}(?:\\.${GO_IDENT})?)?\\(${GO_IDENT}\\)$`,
|
|
118
119
|
`^go://${GO_PATH}#${GO_FILE}:file$`,
|
|
119
120
|
`^go://${GO_PATH}#${GO_PATH}(?:@${GO_VERSION})?:require$`,
|
|
120
121
|
`^go://${GO_PATH}#${GO_PATH}(?:@${GO_VERSION})?:replace$`,
|
|
@@ -243,10 +244,11 @@ function claimKey(finding: Finding): string {
|
|
|
243
244
|
|
|
244
245
|
/**
|
|
245
246
|
* Whether one record names one finding. A record reaches a finding in two ways: it bound
|
|
246
|
-
* the declaration at the finding's position, or it names the finding's reference
|
|
247
|
-
*
|
|
248
|
-
* finding
|
|
249
|
-
*
|
|
247
|
+
* the declaration at the finding's position, or it names the finding's reference, or the
|
|
248
|
+
* reference of the declaration holding a part that has one of its own, and the file the
|
|
249
|
+
* finding is reported in, each exactly. The second is how an entry copied from a finding
|
|
250
|
+
* about a part of a declaration reaches the part, and the only way any record reaches a
|
|
251
|
+
* dependency row.
|
|
250
252
|
*/
|
|
251
253
|
export function pairs(record: SuppressionRecord, finding: Finding): boolean {
|
|
252
254
|
if (record.code !== finding.code) {
|
|
@@ -265,7 +267,10 @@ export function pairs(record: SuppressionRecord, finding: Finding): boolean {
|
|
|
265
267
|
if (record.bound !== "" && record.bound === positionKey(finding.position)) {
|
|
266
268
|
return true;
|
|
267
269
|
}
|
|
268
|
-
return
|
|
270
|
+
return (
|
|
271
|
+
(record.symbol === finding.symbol.ref || record.symbol === finding.symbol.holder) &&
|
|
272
|
+
record.path === finding.position.path
|
|
273
|
+
);
|
|
269
274
|
}
|
|
270
275
|
|
|
271
276
|
/**
|
package/src/sweep.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import type { TSExemptionClass } from "./exempt-classes.ts";
|
|
10
10
|
import { namesACaller, OUTSIDE, type Edge, type Graph } from "./graph.ts";
|
|
11
|
+
import type { InventorySymbol } from "./inventory.ts";
|
|
11
12
|
import type { Position } from "./position.ts";
|
|
12
13
|
|
|
13
14
|
/** The liveness relation that decided a declaration, as a finding spells it. */
|
|
@@ -178,17 +179,32 @@ export function sweep(graph: Graph, input: SweepInput): Liveness {
|
|
|
178
179
|
return { liveUnder: live, candidates };
|
|
179
180
|
}
|
|
180
181
|
|
|
182
|
+
/**
|
|
183
|
+
* The directory a test runner reads a manual mock from, whose exports it puts in place of
|
|
184
|
+
* the module's: https://jestjs.io/docs/manual-mocks
|
|
185
|
+
*/
|
|
186
|
+
const MANUAL_MOCKS = "__mocks__";
|
|
187
|
+
|
|
188
|
+
/** Whether a test runner calls or reads one export of a test file: a function, or any export of a manual mock. */
|
|
189
|
+
export function runnerCalls(symbol: InventorySymbol): boolean {
|
|
190
|
+
return symbol.kind === "function" || symbol.position.path.split("/").includes(MANUAL_MOCKS);
|
|
191
|
+
}
|
|
192
|
+
|
|
181
193
|
/**
|
|
182
194
|
* Per declaration of the graph, whether the run that counts test references reaches it.
|
|
183
|
-
* That run's roots are the graph's own and each test file,
|
|
184
|
-
*
|
|
185
|
-
*
|
|
195
|
+
* That run's roots are the graph's own and, of each test file, its top level and every
|
|
196
|
+
* function it exports, which a test runner may call, and every export of a manual mock,
|
|
197
|
+
* which the runner reads; any other export of a test file, and test-support code, which
|
|
198
|
+
* runs where a test imports it, are judged as any file's are.
|
|
199
|
+
* `held` seeds it as a mark or an exemption seeds a sweep.
|
|
186
200
|
*/
|
|
187
201
|
export function reachedWithTheTests(graph: Graph, held: readonly string[]): readonly boolean[] {
|
|
188
202
|
const tests = graph.symbols.flatMap((symbol, at) => {
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
return graph.test[at] === true &&
|
|
203
|
+
const exported = symbol.exported && graph.symbols[graph.parent[at] ?? OUTSIDE]?.kind === "file";
|
|
204
|
+
const called = exported && runnerCalls(symbol);
|
|
205
|
+
return graph.test[at] === true &&
|
|
206
|
+
graph.support[at] !== true &&
|
|
207
|
+
(symbol.kind === "file" || called)
|
|
192
208
|
? [{ at, kind: "test-runner" as const }]
|
|
193
209
|
: [];
|
|
194
210
|
});
|
|
@@ -277,10 +293,11 @@ function reachable(graph: Graph, held: readonly boolean[], mode: Mode): readonly
|
|
|
277
293
|
|
|
278
294
|
/**
|
|
279
295
|
* Admits every test declaration whose set of referenced production declarations is
|
|
280
|
-
* non-empty and wholly dead, and returns which it admitted.
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
296
|
+
* non-empty and wholly dead, and returns which it admitted. An ambient declaration describes
|
|
297
|
+
* code the target does not implement, so it is no production declaration here. The set is
|
|
298
|
+
* read from every reference the test makes, which a production sweep does not count. A
|
|
299
|
+
* test-support declaration a test referencing a live target also references is a live
|
|
300
|
+
* target of every test referencing it. A marked or exempt test is never admitted.
|
|
284
301
|
*/
|
|
285
302
|
function admitTestsOfDeadCode(
|
|
286
303
|
graph: Graph,
|
|
@@ -293,7 +310,11 @@ function admitTestsOfDeadCode(
|
|
|
293
310
|
let live = 0;
|
|
294
311
|
if (testCode(at) && graph.subject[at] === true && held[at] !== true) {
|
|
295
312
|
for (const edge of graph.out[at] ?? []) {
|
|
296
|
-
if (
|
|
313
|
+
if (
|
|
314
|
+
testCode(edge.to) ||
|
|
315
|
+
graph.subject[edge.to] !== true ||
|
|
316
|
+
graph.ambient[edge.to] === true
|
|
317
|
+
) {
|
|
297
318
|
continue;
|
|
298
319
|
}
|
|
299
320
|
targets += 1;
|
package/src/symbol-keys.ts
CHANGED
|
@@ -2,24 +2,31 @@
|
|
|
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
8
|
|
|
8
9
|
import {
|
|
9
10
|
isComputedPropertyName,
|
|
11
|
+
isForOfStatement,
|
|
10
12
|
isIdentifier,
|
|
11
13
|
isNoSubstitutionTemplateLiteral,
|
|
12
14
|
isNumericLiteral,
|
|
13
15
|
isParenthesizedExpression,
|
|
14
16
|
isPropertyAccessExpression,
|
|
17
|
+
isSpreadElement,
|
|
15
18
|
isStringLiteral,
|
|
16
19
|
type ElementAccessExpression,
|
|
20
|
+
type Expression,
|
|
17
21
|
type Node,
|
|
22
|
+
type SourceFile,
|
|
18
23
|
} from "@typescript/native/ast";
|
|
19
24
|
import type { Symbol as TSSymbol, Type } from "@typescript/native/sync";
|
|
20
25
|
import type { AliasChains } from "./alias-chain.ts";
|
|
21
|
-
import type
|
|
26
|
+
import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
|
|
27
|
+
import { renderPosition } from "./position.ts";
|
|
22
28
|
import { isAnswered, type PropertyTables } from "./query.ts";
|
|
29
|
+
import type { Reference } from "./references.ts";
|
|
23
30
|
import type { ProjectView } from "./session.ts";
|
|
24
31
|
|
|
25
32
|
/** One element access whose key is a name, and how the access uses the member it selects. */
|
|
@@ -170,3 +177,123 @@ export function keyedUses<Brand>(
|
|
|
170
177
|
return ids.length === 0 ? [] : [{ access: one.access, ids: [...new Set(ids)] }];
|
|
171
178
|
});
|
|
172
179
|
}
|
|
180
|
+
|
|
181
|
+
/** The well-known symbol a value's iteration calls, by the construct that iterates it. */
|
|
182
|
+
type Protocol = "iterator" | "asyncIterator";
|
|
183
|
+
|
|
184
|
+
/** The member name `[Symbol.<name>]` spells, where a computed name spells one. */
|
|
185
|
+
function wellKnownName(name: Node | undefined): string | undefined {
|
|
186
|
+
if (name === undefined || !isComputedPropertyName(name)) {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
const key = name.expression;
|
|
190
|
+
return isPropertyAccessExpression(key) &&
|
|
191
|
+
isIdentifier(key.expression) &&
|
|
192
|
+
key.expression.text === "Symbol" &&
|
|
193
|
+
isIdentifier(key.name)
|
|
194
|
+
? key.name.text
|
|
195
|
+
: undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The references the iterations of one project's own files make to the iteration members
|
|
200
|
+
* of the values they iterate: a `for…of` statement and a spread element read
|
|
201
|
+
* `[Symbol.iterator]`, and a `for await…of` statement reads `[Symbol.asyncIterator]` and,
|
|
202
|
+
* which it falls back to, `[Symbol.iterator]`. Each is made by the declaration holding it.
|
|
203
|
+
*/
|
|
204
|
+
export function iterationReferences<Brand>(
|
|
205
|
+
project: ProjectView<Brand>,
|
|
206
|
+
held: Inventory,
|
|
207
|
+
chains: AliasChains,
|
|
208
|
+
root: string,
|
|
209
|
+
testFiles: ReadonlySet<string>,
|
|
210
|
+
): readonly Reference[] {
|
|
211
|
+
const queries = project.queries;
|
|
212
|
+
const ownPaths = project.ownPaths();
|
|
213
|
+
const iterated: {
|
|
214
|
+
readonly file: SourceFile;
|
|
215
|
+
readonly from: string;
|
|
216
|
+
readonly operand: Expression;
|
|
217
|
+
readonly protocols: readonly Protocol[];
|
|
218
|
+
}[] = [];
|
|
219
|
+
for (const file of project.ownSourceFiles()) {
|
|
220
|
+
const fileId = held.declarations.get(nodeKey(file, file));
|
|
221
|
+
if (fileId === undefined) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const visit = (node: Node, from: string): void => {
|
|
225
|
+
const holder = held.declarations.get(nodeKey(file, node)) ?? from;
|
|
226
|
+
if (isForOfStatement(node)) {
|
|
227
|
+
const protocols: readonly Protocol[] =
|
|
228
|
+
node.awaitModifier === undefined ? ["iterator"] : ["asyncIterator", "iterator"];
|
|
229
|
+
iterated.push({ file, from: holder, operand: node.expression, protocols });
|
|
230
|
+
} else if (isSpreadElement(node)) {
|
|
231
|
+
iterated.push({ file, from: holder, operand: node.expression, protocols: ["iterator"] });
|
|
232
|
+
}
|
|
233
|
+
node.forEachChild((child) => {
|
|
234
|
+
visit(child, holder);
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
file.forEachChild((child) => {
|
|
238
|
+
visit(child, fileId);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
if (iterated.length === 0) {
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
const types = queries.typesAt(iterated.map((one) => one.operand));
|
|
245
|
+
const members = new Map<
|
|
246
|
+
number,
|
|
247
|
+
readonly { readonly name: string; readonly ids: readonly string[] }[]
|
|
248
|
+
>();
|
|
249
|
+
const membersOf = (
|
|
250
|
+
type: Type,
|
|
251
|
+
): readonly { readonly name: string; readonly ids: readonly string[] }[] => {
|
|
252
|
+
const known = members.get(type.id);
|
|
253
|
+
if (known !== undefined) {
|
|
254
|
+
return known;
|
|
255
|
+
}
|
|
256
|
+
const keyed: { name: string; ids: readonly string[] }[] = [];
|
|
257
|
+
const properties = queries.propertiesOf(type);
|
|
258
|
+
for (const property of isAnswered(properties) ? properties : []) {
|
|
259
|
+
for (const handle of property.declarations) {
|
|
260
|
+
const node = ownPaths.has(handle.path) ? project.declarationAt(handle)?.node : undefined;
|
|
261
|
+
const name = wellKnownName((node as { readonly name?: Node } | undefined)?.name);
|
|
262
|
+
if (name !== undefined) {
|
|
263
|
+
keyed.push({ name, ids: chains.declarationsOf(property) });
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
members.set(type.id, keyed);
|
|
269
|
+
return keyed;
|
|
270
|
+
};
|
|
271
|
+
const found: Reference[] = [];
|
|
272
|
+
iterated.forEach((one, index) => {
|
|
273
|
+
const type = types[index];
|
|
274
|
+
if (type === undefined || !isAnswered(type)) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const parts =
|
|
278
|
+
type.isUnionType() || type.isIntersectionType() ? queries.constituents(type) : [type];
|
|
279
|
+
const position = renderPosition(one.file, root, one.operand.getStart());
|
|
280
|
+
for (const part of isAnswered(parts) ? parts : []) {
|
|
281
|
+
for (const member of membersOf(part)) {
|
|
282
|
+
if (!(one.protocols as readonly string[]).includes(member.name)) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
for (const to of member.ids) {
|
|
286
|
+
found.push({
|
|
287
|
+
from: one.from,
|
|
288
|
+
to,
|
|
289
|
+
position,
|
|
290
|
+
use: "read",
|
|
291
|
+
resolution: "keyed",
|
|
292
|
+
test: testFiles.has(position.path),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
return found;
|
|
299
|
+
}
|
package/src/test-support.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
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
|
*/
|
|
7
8
|
|
|
9
|
+
import { CONVENTION_ROWS, TEST_LIBRARIES, TEST_LIBRARY_SCOPES } from "./convention-rows.ts";
|
|
10
|
+
import { packageOfSpecifier } from "./dependencies.ts";
|
|
8
11
|
import type { InventorySymbol } from "./inventory.ts";
|
|
9
12
|
import type { Reference } from "./references.ts";
|
|
10
13
|
import type { Root } from "./roots.ts";
|
|
@@ -12,19 +15,144 @@ import type { Root } from "./roots.ts";
|
|
|
12
15
|
/** The root kind that never takes a file out of test-support code: the skip only withholds. */
|
|
13
16
|
const SKIP_ROOT = "type-error";
|
|
14
17
|
|
|
18
|
+
/**
|
|
19
|
+
* The test runners and test libraries an import of which is test evidence: each runner a
|
|
20
|
+
* convention row reads test files for, and the convention table's test libraries.
|
|
21
|
+
*/
|
|
22
|
+
const TEST_PACKAGES: ReadonlySet<string> = new Set([
|
|
23
|
+
...CONVENTION_ROWS.filter((row) => row.testFiles !== undefined).map((row) => row.package),
|
|
24
|
+
...TEST_LIBRARIES,
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The Node.js built-in modules a bare specifier names, which are no package. The set is
|
|
29
|
+
* closed: Node.js adds a new built-in under the `node:` scheme alone
|
|
30
|
+
* (https://nodejs.org/api/modules.html#built-in-modules-with-mandatory-node-prefix).
|
|
31
|
+
*/
|
|
32
|
+
const NODE_BUILTINS: ReadonlySet<string> = new Set([
|
|
33
|
+
"assert",
|
|
34
|
+
"async_hooks",
|
|
35
|
+
"buffer",
|
|
36
|
+
"child_process",
|
|
37
|
+
"cluster",
|
|
38
|
+
"console",
|
|
39
|
+
"constants",
|
|
40
|
+
"crypto",
|
|
41
|
+
"dgram",
|
|
42
|
+
"diagnostics_channel",
|
|
43
|
+
"dns",
|
|
44
|
+
"domain",
|
|
45
|
+
"events",
|
|
46
|
+
"fs",
|
|
47
|
+
"http",
|
|
48
|
+
"http2",
|
|
49
|
+
"https",
|
|
50
|
+
"inspector",
|
|
51
|
+
"module",
|
|
52
|
+
"net",
|
|
53
|
+
"os",
|
|
54
|
+
"path",
|
|
55
|
+
"perf_hooks",
|
|
56
|
+
"process",
|
|
57
|
+
"punycode",
|
|
58
|
+
"querystring",
|
|
59
|
+
"readline",
|
|
60
|
+
"repl",
|
|
61
|
+
"stream",
|
|
62
|
+
"string_decoder",
|
|
63
|
+
"sys",
|
|
64
|
+
"timers",
|
|
65
|
+
"tls",
|
|
66
|
+
"trace_events",
|
|
67
|
+
"tty",
|
|
68
|
+
"url",
|
|
69
|
+
"util",
|
|
70
|
+
"v8",
|
|
71
|
+
"vm",
|
|
72
|
+
"wasi",
|
|
73
|
+
"worker_threads",
|
|
74
|
+
"zlib",
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
/** What a file imports, read for test evidence. */
|
|
78
|
+
export interface TestEvidence {
|
|
79
|
+
/** Per file below the target root, the module specifiers its imports write. */
|
|
80
|
+
readonly imports: ReadonlyMap<string, readonly string[]>;
|
|
81
|
+
/**
|
|
82
|
+
* Whether the manifest nearest the file, below the target root, that declares the
|
|
83
|
+
* package declares it under `devDependencies`.
|
|
84
|
+
*/
|
|
85
|
+
readonly devDependency: (path: string, name: string) => boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Whether a specifier names a test runner or a test library. */
|
|
89
|
+
function namesTestPackage(specifier: string): boolean {
|
|
90
|
+
const name = specifier.startsWith("node:") ? specifier : packageOfSpecifier(specifier);
|
|
91
|
+
return (
|
|
92
|
+
name !== undefined &&
|
|
93
|
+
(TEST_PACKAGES.has(name) || TEST_LIBRARY_SCOPES.some((scope) => name.startsWith(scope)))
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Whether a file carries test evidence: an import of a test runner or a test library, or
|
|
99
|
+
* package imports, Node built-ins aside, that are all devDependencies no file imports
|
|
100
|
+
* outside the test files and `onlyTests`, the files only tests reach.
|
|
101
|
+
*/
|
|
102
|
+
function evidenceReader(
|
|
103
|
+
tests: ReadonlySet<string>,
|
|
104
|
+
evidence: TestEvidence,
|
|
105
|
+
): (onlyTests: ReadonlySet<string>, path: string) => boolean {
|
|
106
|
+
const importers = new Map<string, string[]>();
|
|
107
|
+
for (const [path, specifiers] of evidence.imports) {
|
|
108
|
+
for (const specifier of specifiers) {
|
|
109
|
+
const name = packageOfSpecifier(specifier);
|
|
110
|
+
if (name !== undefined) {
|
|
111
|
+
importers.set(name, [...(importers.get(name) ?? []), path]);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return (onlyTests, path) => {
|
|
116
|
+
const specifiers = evidence.imports.get(path) ?? [];
|
|
117
|
+
if (specifiers.some(namesTestPackage)) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
const names = specifiers.flatMap((one) => packageOfSpecifier(one) ?? []);
|
|
121
|
+
const packages = names.filter((name) => !NODE_BUILTINS.has(name));
|
|
122
|
+
return (
|
|
123
|
+
packages.length > 0 &&
|
|
124
|
+
packages.every(
|
|
125
|
+
(name) =>
|
|
126
|
+
evidence.devDependency(path, name) &&
|
|
127
|
+
(importers.get(name) ?? []).every((one) => tests.has(one) || onlyTests.has(one)),
|
|
128
|
+
)
|
|
129
|
+
);
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The files only tests reach, split by whether each carries test evidence of its own. */
|
|
134
|
+
interface TestReached {
|
|
135
|
+
/** The test-support files: test code. */
|
|
136
|
+
readonly support: ReadonlySet<string>;
|
|
137
|
+
/** The files only tests reach that are no test-support file: production code only tests use. */
|
|
138
|
+
readonly testUsed: ReadonlySet<string>;
|
|
139
|
+
}
|
|
140
|
+
|
|
15
141
|
/**
|
|
16
142
|
* The files of one project that are test-support code, below the target root: the
|
|
17
143
|
* largest set of files a test file reaches, none of them a test file or holding a root,
|
|
18
|
-
* whose every user outside the set is a test
|
|
19
|
-
*
|
|
20
|
-
*
|
|
144
|
+
* each carrying test evidence of its own, whose every user outside the set is a test
|
|
145
|
+
* file; and the files only tests reach that are not among them. A root has a caller
|
|
146
|
+
* outside the reference graph: a runtime, a tool, a configuration, or a consumer of a
|
|
147
|
+
* library's published API. `testFiles` are the files the test-file rules classified.
|
|
21
148
|
*/
|
|
22
149
|
export function testSupportFiles(
|
|
23
150
|
symbols: readonly InventorySymbol[],
|
|
24
151
|
references: readonly Reference[],
|
|
25
152
|
roots: readonly Root[],
|
|
26
153
|
testFiles: readonly string[],
|
|
27
|
-
|
|
154
|
+
evidence: TestEvidence,
|
|
155
|
+
): TestReached {
|
|
28
156
|
const fileOf = new Map(symbols.map((symbol) => [symbol.id, symbol.position.path]));
|
|
29
157
|
const users = new Map<string, Set<string>>();
|
|
30
158
|
const uses = new Map<string, Set<string>>();
|
|
@@ -59,18 +187,27 @@ export function testSupportFiles(
|
|
|
59
187
|
}
|
|
60
188
|
}
|
|
61
189
|
}
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
190
|
+
const withoutOutsideUsers = (files: Set<string>): Set<string> => {
|
|
191
|
+
for (let changed = true; changed;) {
|
|
192
|
+
changed = false;
|
|
193
|
+
for (const path of files) {
|
|
194
|
+
const from = users.get(path) ?? new Set<string>();
|
|
195
|
+
if (![...from].every((one) => tests.has(one) || files.has(one))) {
|
|
196
|
+
files.delete(path);
|
|
197
|
+
changed = true;
|
|
198
|
+
}
|
|
70
199
|
}
|
|
71
200
|
}
|
|
72
|
-
|
|
73
|
-
|
|
201
|
+
return files;
|
|
202
|
+
};
|
|
203
|
+
// Evidence reads the files only tests reach, evidence or none, so it is fixed before the
|
|
204
|
+
// files without evidence leave the set.
|
|
205
|
+
const onlyTests = withoutOutsideUsers(new Set([...reached].filter((path) => !rooted.has(path))));
|
|
206
|
+
const carries = evidenceReader(tests, evidence);
|
|
207
|
+
const support = withoutOutsideUsers(
|
|
208
|
+
new Set([...onlyTests].filter((path) => carries(onlyTests, path))),
|
|
209
|
+
);
|
|
210
|
+
return { support, testUsed: new Set([...onlyTests].filter((path) => !support.has(path))) };
|
|
74
211
|
}
|
|
75
212
|
|
|
76
213
|
/** The references with every one a test-support file makes counted as a test reference. */
|
package/src/type-errors.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
isClassDeclaration,
|
|
11
|
+
isFunctionLikeDeclaration,
|
|
11
12
|
isNoSubstitutionTemplateLiteral,
|
|
12
13
|
isPropertyAccessExpression,
|
|
13
14
|
isStringLiteral,
|
|
@@ -25,6 +26,7 @@ import { isAnswered } from "./query.ts";
|
|
|
25
26
|
import type { TypeErrorSkip } from "./report.ts";
|
|
26
27
|
import type { Engine, ProjectView } from "./session.ts";
|
|
27
28
|
import type { SetupFailure } from "./setup-failure.ts";
|
|
29
|
+
import { findWorkspace, type Workspace } from "./workspace.ts";
|
|
28
30
|
|
|
29
31
|
/** The compiler's codes for an import whose module it cannot find. */
|
|
30
32
|
const MODULE_NOT_FOUND: ReadonlySet<number> = new Set([2307, 2792]);
|
|
@@ -78,6 +80,8 @@ interface ErrorContext {
|
|
|
78
80
|
readonly targetRoot: string;
|
|
79
81
|
/** The project configuration's `paths` mapping. */
|
|
80
82
|
readonly paths: PathsMapping;
|
|
83
|
+
/** Whether one manifest is a workspace member's, whose dependencies the workspace installs. */
|
|
84
|
+
readonly member: (manifest: string) => boolean;
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
/** One configuration's `paths` mapping, its targets read against `base`. */
|
|
@@ -241,7 +245,11 @@ function expectedModule(
|
|
|
241
245
|
name === undefined
|
|
242
246
|
? undefined
|
|
243
247
|
: declaringManifest(host, dirnamePath(importer), targetRoot, name);
|
|
244
|
-
|
|
248
|
+
// No run requires a nested package that is no workspace member to be installed.
|
|
249
|
+
if (
|
|
250
|
+
manifest === undefined ||
|
|
251
|
+
(dirnamePath(manifest) !== targetRoot && !context.member(manifest))
|
|
252
|
+
) {
|
|
245
253
|
return undefined;
|
|
246
254
|
}
|
|
247
255
|
const where = relativePath(targetRoot, manifest) ?? manifest;
|
|
@@ -249,13 +257,27 @@ function expectedModule(
|
|
|
249
257
|
}
|
|
250
258
|
|
|
251
259
|
/**
|
|
252
|
-
* The unit a type error at one offset skips: the
|
|
253
|
-
* top-level class does, else the top-level statement.
|
|
260
|
+
* The unit a type error at one offset skips: the innermost function holding it, else the
|
|
261
|
+
* class member holding it where a top-level class does, else the top-level statement.
|
|
254
262
|
*/
|
|
255
263
|
function unitAt(file: SourceFile, offset: number): Node | undefined {
|
|
256
264
|
const statement = file.statements.find((one) => one.pos <= offset && offset < one.end);
|
|
257
|
-
if (statement === undefined
|
|
258
|
-
return
|
|
265
|
+
if (statement === undefined) {
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
let innermost: Node | undefined;
|
|
269
|
+
const visit = (node: Node): void => {
|
|
270
|
+
if (offset < node.getStart() || offset >= node.end) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (isFunctionLikeDeclaration(node)) {
|
|
274
|
+
innermost = node;
|
|
275
|
+
}
|
|
276
|
+
node.forEachChild(visit);
|
|
277
|
+
};
|
|
278
|
+
visit(statement);
|
|
279
|
+
if (innermost !== undefined || !isClassDeclaration(statement)) {
|
|
280
|
+
return innermost ?? statement;
|
|
259
281
|
}
|
|
260
282
|
return statement.members.find((one) => one.pos <= offset && offset < one.end) ?? statement;
|
|
261
283
|
}
|
|
@@ -274,8 +296,18 @@ function messageOf(text: string, targetRoot: string): string {
|
|
|
274
296
|
export function readErrors<Brand>(
|
|
275
297
|
project: ProjectView<Brand>,
|
|
276
298
|
errors: readonly LocatedDiagnostic[],
|
|
277
|
-
|
|
299
|
+
where: Omit<ErrorContext, "member">,
|
|
278
300
|
): ProjectErrorReading {
|
|
301
|
+
let workspace: Workspace | undefined | null = null;
|
|
302
|
+
const context: ErrorContext = {
|
|
303
|
+
...where,
|
|
304
|
+
member: (manifest) => {
|
|
305
|
+
if (workspace === null) {
|
|
306
|
+
workspace = findWorkspace(where.host, where.targetRoot, () => undefined);
|
|
307
|
+
}
|
|
308
|
+
return workspace?.memberOf(manifest) !== undefined;
|
|
309
|
+
},
|
|
310
|
+
};
|
|
279
311
|
const files = new Map(project.ownSourceFiles().map((file) => [file.fileName, file]));
|
|
280
312
|
const missing: SetupFailure[] = [];
|
|
281
313
|
const skips: Skip[] = [];
|