@cplieger/deadset-ts 5.1.0 → 5.2.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 +1 -1
- package/dist/src/analysis.js +79 -37
- package/dist/src/config.js +3 -0
- package/dist/src/configuration-files.js +24 -25
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/contextual-uses.js +46 -18
- package/dist/src/convention-rows.js +7 -0
- package/dist/src/conventions.js +97 -33
- package/dist/src/dependencies.js +8 -0
- package/dist/src/entry-point-gaps.js +10 -26
- package/dist/src/entry-points.js +4 -0
- package/dist/src/exempt.js +23 -8
- package/dist/src/file-facts.js +23 -20
- package/dist/src/findings/completion.js +27 -11
- package/dist/src/findings/interfaces.js +2 -6
- package/dist/src/findings/intra-function.js +15 -3
- package/dist/src/findings/unused-declarations.js +5 -6
- package/dist/src/findings/visibility-narrowing.js +6 -1
- package/dist/src/generated-file.js +72 -12
- package/dist/src/graph.js +6 -5
- package/dist/src/html-entries.js +137 -0
- package/dist/src/inventory.js +13 -3
- package/dist/src/kinds.js +2 -2
- package/dist/src/manifest.js +34 -5
- package/dist/src/matrix.js +9 -3
- package/dist/src/namespace-objects.js +158 -0
- package/dist/src/outside-tests.js +139 -0
- package/dist/src/overrides.js +104 -0
- package/dist/src/paths.js +17 -0
- package/dist/src/references.js +150 -4
- package/dist/src/roots.js +172 -39
- package/dist/src/session.js +4 -3
- package/dist/src/sweep.js +17 -0
- package/dist/src/template-field.js +60 -3
- package/dist/src/test-support.js +35 -23
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +122 -49
- package/src/config.ts +3 -0
- package/src/configuration-files.ts +29 -25
- package/src/conformance-record.ts +2 -2
- package/src/contextual-uses.ts +51 -20
- package/src/convention-rows.ts +15 -0
- package/src/conventions.ts +119 -30
- package/src/dependencies.ts +9 -0
- package/src/entry-point-gaps.ts +11 -28
- package/src/entry-points.ts +7 -1
- package/src/exempt.ts +29 -12
- package/src/file-facts.ts +25 -21
- package/src/findings/completion.ts +31 -11
- package/src/findings/interfaces.ts +2 -9
- package/src/findings/intra-function.ts +17 -3
- package/src/findings/unused-declarations.ts +5 -8
- package/src/findings/visibility-narrowing.ts +7 -1
- package/src/generated-file.ts +80 -12
- package/src/graph.ts +9 -8
- package/src/html-entries.ts +170 -0
- package/src/inventory.ts +15 -3
- package/src/kinds.ts +2 -2
- package/src/manifest.ts +49 -7
- package/src/matrix.ts +21 -3
- package/src/namespace-objects.ts +216 -0
- package/src/outside-tests.ts +191 -0
- package/src/overrides.ts +136 -0
- package/src/paths.ts +18 -0
- package/src/references.ts +206 -9
- package/src/roots.ts +210 -53
- package/src/session.ts +4 -2
- package/src/sweep.ts +19 -0
- package/src/template-field.ts +65 -3
- package/src/test-support.ts +34 -22
- package/src/version.ts +1 -1
package/src/roots.ts
CHANGED
|
@@ -8,7 +8,16 @@
|
|
|
8
8
|
* under reachability and still a candidate under reference counting.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import
|
|
11
|
+
import {
|
|
12
|
+
isExpressionWithTypeArguments,
|
|
13
|
+
isFunctionLikeDeclaration,
|
|
14
|
+
isPropertyAccessExpression,
|
|
15
|
+
isQualifiedName,
|
|
16
|
+
isTypeQueryNode,
|
|
17
|
+
isTypeReferenceNode,
|
|
18
|
+
type Node,
|
|
19
|
+
type SourceFile,
|
|
20
|
+
} from "@typescript/native/unstable/ast";
|
|
12
21
|
import { aliasChains } from "./alias-chain.ts";
|
|
13
22
|
import type { ParseFile } from "./configuration-files.ts";
|
|
14
23
|
import { conventionEntries, type Conventions } from "./conventions.ts";
|
|
@@ -17,6 +26,7 @@ import { globExpression } from "./glob.ts";
|
|
|
17
26
|
import type { Host } from "./host.ts";
|
|
18
27
|
import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
|
|
19
28
|
import type { Manifest, ManifestEntry } from "./manifest.ts";
|
|
29
|
+
import { relativePath, resolvePath } from "./paths.ts";
|
|
20
30
|
import { byPosition } from "./position.ts";
|
|
21
31
|
import { UNANSWERED } from "./query.ts";
|
|
22
32
|
import type { ProjectView } from "./session.ts";
|
|
@@ -88,8 +98,8 @@ export interface Roots {
|
|
|
88
98
|
readonly unmatched: readonly string[];
|
|
89
99
|
/**
|
|
90
100
|
* Every declaration of the project, where the checker left the module or export table
|
|
91
|
-
* of an entry or a published file unanswered,
|
|
92
|
-
*
|
|
101
|
+
* of an entry or a published file unanswered, since which of them that file exports is
|
|
102
|
+
* unknown; otherwise each one an unanswered published type name may name.
|
|
93
103
|
*/
|
|
94
104
|
readonly unanswered: readonly string[];
|
|
95
105
|
}
|
|
@@ -101,10 +111,10 @@ export interface RootOptions {
|
|
|
101
111
|
/** The target's own manifest. */
|
|
102
112
|
readonly manifest: Manifest;
|
|
103
113
|
/**
|
|
104
|
-
* The entry points the other packages
|
|
105
|
-
* marked published or not by its own manifest's rule.
|
|
114
|
+
* The entry points the target's other packages name, its workspace's members and the
|
|
115
|
+
* packages below it, each already marked published or not by its own manifest's rule.
|
|
106
116
|
*/
|
|
107
|
-
readonly
|
|
117
|
+
readonly otherPackages?: readonly ManifestEntry[] | undefined;
|
|
108
118
|
/** `roots.patterns` from the resolved configuration. */
|
|
109
119
|
readonly patterns: readonly string[];
|
|
110
120
|
/** `ts.entry_files` from the resolved configuration. */
|
|
@@ -126,6 +136,50 @@ const ENTRY_KINDS = {
|
|
|
126
136
|
script: "script",
|
|
127
137
|
} as const satisfies Record<ManifestEntry["role"], RootKind>;
|
|
128
138
|
|
|
139
|
+
/** The rules whose rooted files a manifest target is read back to by name. */
|
|
140
|
+
const READ_BACK_RULES: ReadonlySet<EntryRule> = new Set([
|
|
141
|
+
"configuration-file",
|
|
142
|
+
"configuration-string",
|
|
143
|
+
"html-entry",
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
/** The extensions a manifest target's name drops: the ones a build writes. */
|
|
147
|
+
const OUTPUT_EXTENSION = /\.(?:d\.ts|d\.mts|d\.cts|js|mjs|cjs|jsx)$/u;
|
|
148
|
+
|
|
149
|
+
/** The extension a source file's name drops. */
|
|
150
|
+
const SOURCE_EXTENSION = /\.(?:d\.)?[cm]?[jt]sx?$/u;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The rooted files one manifest target reads back to by name: its path below the
|
|
154
|
+
* manifest's directory, past its first directory where it names one, without its output
|
|
155
|
+
* extension, is the name, and a rooted file is one whose path below the same directory,
|
|
156
|
+
* without its source extension, ends with that name at a directory boundary.
|
|
157
|
+
*/
|
|
158
|
+
function readBackByName(
|
|
159
|
+
host: Host,
|
|
160
|
+
entry: ManifestEntry,
|
|
161
|
+
rooted: readonly SourceFile[],
|
|
162
|
+
): readonly SourceFile[] {
|
|
163
|
+
const dir = resolvePath(host.workingDirectory(), entry.dir);
|
|
164
|
+
const below = relativePath(dir, resolvePath(host.workingDirectory(), entry.path));
|
|
165
|
+
if (below === undefined || below === "") {
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
const cut = below.indexOf("/");
|
|
169
|
+
const name = (cut === -1 ? below : below.slice(cut + 1)).replace(OUTPUT_EXTENSION, "");
|
|
170
|
+
if (name === "") {
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
const found = new Map<string, SourceFile>();
|
|
174
|
+
for (const file of rooted) {
|
|
175
|
+
const path = relativePath(dir, file.fileName)?.replace(SOURCE_EXTENSION, "");
|
|
176
|
+
if (path !== undefined && (path === name || path.endsWith(`/${name}`))) {
|
|
177
|
+
found.set(file.fileName, file);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return [...found.values()];
|
|
181
|
+
}
|
|
182
|
+
|
|
129
183
|
/** Whether a configured string carries either special character. */
|
|
130
184
|
function isPattern(text: string): boolean {
|
|
131
185
|
return text.includes("*") || text.includes("?");
|
|
@@ -236,6 +290,14 @@ export function roots<Brand>(
|
|
|
236
290
|
entered.push({ file, kind, source, exports });
|
|
237
291
|
};
|
|
238
292
|
|
|
293
|
+
const points = entryPoints(
|
|
294
|
+
project,
|
|
295
|
+
files,
|
|
296
|
+
options.testFiles,
|
|
297
|
+
options.host,
|
|
298
|
+
targetRoot,
|
|
299
|
+
options.parse,
|
|
300
|
+
);
|
|
239
301
|
const published: SourceFile[] = [];
|
|
240
302
|
const entries = [
|
|
241
303
|
...options.manifest.entries.map((entry) => ({
|
|
@@ -244,16 +306,29 @@ export function roots<Brand>(
|
|
|
244
306
|
entry.published &&
|
|
245
307
|
(options.manifest.declaresExports ? entry.member.startsWith("exports") : true),
|
|
246
308
|
})),
|
|
247
|
-
...(options.
|
|
309
|
+
...(options.otherPackages ?? []).map((entry) => ({ entry, publishes: entry.published })),
|
|
310
|
+
];
|
|
311
|
+
const scripted = entries.flatMap(({ entry }) =>
|
|
312
|
+
entry.role === "script" ? files.named(entry.path) : [],
|
|
313
|
+
);
|
|
314
|
+
const readBackTo = [
|
|
315
|
+
...scripted,
|
|
316
|
+
...points.filter((point) => READ_BACK_RULES.has(point.rule)).map((point) => point.file),
|
|
248
317
|
];
|
|
318
|
+
const entryFiles = (entry: ManifestEntry): readonly SourceFile[] => {
|
|
319
|
+
const named = files.named(entry.path);
|
|
320
|
+
return named.length > 0 || entry.role === "script"
|
|
321
|
+
? named
|
|
322
|
+
: readBackByName(options.host, entry, readBackTo);
|
|
323
|
+
};
|
|
249
324
|
for (const { entry, publishes } of entries) {
|
|
250
325
|
if (options.publishedAPI && publishes) {
|
|
251
|
-
published.push(...
|
|
326
|
+
published.push(...entryFiles(entry));
|
|
252
327
|
}
|
|
253
328
|
}
|
|
254
329
|
const publishedNames = new Set(published.map((file) => file.fileName));
|
|
255
330
|
for (const { entry } of entries) {
|
|
256
|
-
for (const file of
|
|
331
|
+
for (const file of entryFiles(entry)) {
|
|
257
332
|
const kind = ENTRY_KINDS[entry.role];
|
|
258
333
|
enter(file, kind, entry.member, !publishedNames.has(file.fileName));
|
|
259
334
|
}
|
|
@@ -271,14 +346,7 @@ export function roots<Brand>(
|
|
|
271
346
|
: conventionEntries(options.conventions, files)) {
|
|
272
347
|
enter(file, "convention", row, true);
|
|
273
348
|
}
|
|
274
|
-
for (const point of
|
|
275
|
-
project,
|
|
276
|
-
files,
|
|
277
|
-
options.testFiles,
|
|
278
|
-
options.host,
|
|
279
|
-
targetRoot,
|
|
280
|
-
options.parse,
|
|
281
|
-
)) {
|
|
349
|
+
for (const point of points) {
|
|
282
350
|
enter(point.file, point.rule, point.source, point.exports);
|
|
283
351
|
}
|
|
284
352
|
for (const file of files.byPath.values()) {
|
|
@@ -310,10 +378,11 @@ export function roots<Brand>(
|
|
|
310
378
|
}
|
|
311
379
|
}
|
|
312
380
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
381
|
+
const api = options.publishedAPI
|
|
382
|
+
? publishedAPI(project, published, exportsOf, held, files.byPath)
|
|
383
|
+
: { ids: [], guessed: [] };
|
|
384
|
+
for (const id of api.ids) {
|
|
385
|
+
add(id, "published-api", "");
|
|
317
386
|
}
|
|
318
387
|
|
|
319
388
|
const unmatched = configured(held, options.patterns, add);
|
|
@@ -330,7 +399,7 @@ export function roots<Brand>(
|
|
|
330
399
|
configFile: project.configFile,
|
|
331
400
|
liveUnderReachability: ordered,
|
|
332
401
|
unmatched,
|
|
333
|
-
unanswered: answered ?
|
|
402
|
+
unanswered: answered ? api.guessed : held.symbols.map((symbol) => symbol.id),
|
|
334
403
|
};
|
|
335
404
|
}
|
|
336
405
|
|
|
@@ -388,55 +457,143 @@ function exportTables<Brand>(
|
|
|
388
457
|
const EXPORTING_CONTAINERS: ReadonlySet<string> = new Set(["file", "namespace"]);
|
|
389
458
|
|
|
390
459
|
/**
|
|
391
|
-
* Every declaration a consumer of the library can name: what the published files
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
* name. Below a published module or namespace the walk follows what its export table
|
|
398
|
-
* names; below any other published declaration it follows every member a consumer
|
|
399
|
-
* can write: a public one, and a protected one, which a consumer's subclass names. A
|
|
400
|
-
* private member is not published, nor is anything below one, and a type parameter
|
|
401
|
-
* is not published either, because a consumer supplies a type argument by position.
|
|
402
|
-
*
|
|
403
|
-
* A published file is not itself published. A consumer imports a module and names
|
|
404
|
-
* what it exports; the file is a root because the manifest names it, which is a
|
|
405
|
-
* different reason.
|
|
460
|
+
* Every declaration a consumer of the library can name: what the published files export
|
|
461
|
+
* and, below each, the members a consumer writes: a namespace's exported ones, any other
|
|
462
|
+
* declaration's public and protected ones, never a private one or a type parameter. A
|
|
463
|
+
* declaration a published one's type names is published too, exported or not. A type name
|
|
464
|
+
* the checker leaves unanswered publishes every declaration spelled as it is, and those are
|
|
465
|
+
* `guessed`. A published file is not itself published.
|
|
406
466
|
*/
|
|
407
|
-
function publishedAPI(
|
|
467
|
+
function publishedAPI<Brand>(
|
|
468
|
+
project: ProjectView<Brand>,
|
|
408
469
|
published: readonly SourceFile[],
|
|
409
470
|
exportsOf: ReadonlyMap<string, readonly string[]>,
|
|
410
471
|
held: Inventory,
|
|
411
|
-
|
|
472
|
+
byPath: ReadonlyMap<string, SourceFile>,
|
|
473
|
+
): { readonly ids: readonly string[]; readonly guessed: readonly string[] } {
|
|
412
474
|
const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
|
|
413
475
|
const children = childrenOf(held);
|
|
476
|
+
const nodes = declarationNodes(held, byPath);
|
|
477
|
+
const chains = aliasChains(project, held);
|
|
414
478
|
const reached = new Set<string>();
|
|
479
|
+
const guessed = new Set<string>();
|
|
415
480
|
const pending: string[] = [];
|
|
481
|
+
const reach = (id: string): void => {
|
|
482
|
+
if (!reached.has(id) && byId.get(id)?.kind !== "file") {
|
|
483
|
+
reached.add(id);
|
|
484
|
+
pending.push(id);
|
|
485
|
+
}
|
|
486
|
+
};
|
|
416
487
|
for (const file of published) {
|
|
417
488
|
for (const id of exportsOf.get(file.fileName) ?? []) {
|
|
418
489
|
if (byId.get(id)?.kind === "file") {
|
|
419
490
|
pending.push(id);
|
|
420
|
-
} else
|
|
421
|
-
|
|
422
|
-
pending.push(id);
|
|
491
|
+
} else {
|
|
492
|
+
reach(id);
|
|
423
493
|
}
|
|
424
494
|
}
|
|
425
495
|
}
|
|
426
|
-
|
|
427
|
-
const
|
|
428
|
-
for (
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
496
|
+
while (pending.length > 0) {
|
|
497
|
+
const round: string[] = [];
|
|
498
|
+
for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
|
|
499
|
+
round.push(next);
|
|
500
|
+
const exporting = EXPORTING_CONTAINERS.has(byId.get(next)?.kind ?? "");
|
|
501
|
+
for (const member of children.get(next) ?? []) {
|
|
502
|
+
const reachable = exporting
|
|
503
|
+
? member.exported
|
|
504
|
+
: member.visibility !== "private" && member.visibility !== "private-name";
|
|
505
|
+
if (member.kind !== "type-parameter" && reachable) {
|
|
506
|
+
reach(member.id);
|
|
507
|
+
}
|
|
434
508
|
}
|
|
435
|
-
reached.add(member.id);
|
|
436
|
-
pending.push(member.id);
|
|
437
509
|
}
|
|
510
|
+
const named = round.flatMap((id) => {
|
|
511
|
+
const symbol = byId.get(id);
|
|
512
|
+
return symbol === undefined || symbol.kind === "file" ? [] : nodes.typeNamesOf(symbol);
|
|
513
|
+
});
|
|
514
|
+
project.symbolsAt(named.map((node) => project.handle(node))).forEach((symbol, index) => {
|
|
515
|
+
if (symbol === UNANSWERED) {
|
|
516
|
+
for (const id of held.byName.get(named[index]?.getText() ?? "") ?? []) {
|
|
517
|
+
guessed.add(id);
|
|
518
|
+
reach(id);
|
|
519
|
+
}
|
|
520
|
+
} else if (symbol !== undefined) {
|
|
521
|
+
for (const target of chains.chainOf(symbol)) {
|
|
522
|
+
reach(target.id);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
});
|
|
438
526
|
}
|
|
439
|
-
return [...reached];
|
|
527
|
+
return { ids: [...reached], guessed: [...guessed] };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** The type names declarations of the inventory write, each file walked once, when first asked. */
|
|
531
|
+
function declarationNodes(
|
|
532
|
+
held: Inventory,
|
|
533
|
+
byPath: ReadonlyMap<string, SourceFile>,
|
|
534
|
+
): { typeNamesOf(symbol: InventorySymbol): readonly Node[] } {
|
|
535
|
+
const walked = new Map<string, { byId: Map<string, Node>; nodes: Set<Node> }>();
|
|
536
|
+
const nodesIn = (path: string): { byId: Map<string, Node>; nodes: Set<Node> } => {
|
|
537
|
+
const known = walked.get(path);
|
|
538
|
+
if (known !== undefined) {
|
|
539
|
+
return known;
|
|
540
|
+
}
|
|
541
|
+
const found = { byId: new Map<string, Node>(), nodes: new Set<Node>() };
|
|
542
|
+
const file = byPath.get(path);
|
|
543
|
+
if (file !== undefined) {
|
|
544
|
+
const visit = (node: Node): void => {
|
|
545
|
+
const id = held.declarations.get(nodeKey(file, node));
|
|
546
|
+
if (id !== undefined) {
|
|
547
|
+
found.nodes.add(node);
|
|
548
|
+
if (!found.byId.has(id)) {
|
|
549
|
+
found.byId.set(id, node);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
node.forEachChild(visit);
|
|
553
|
+
};
|
|
554
|
+
visit(file);
|
|
555
|
+
}
|
|
556
|
+
walked.set(path, found);
|
|
557
|
+
return found;
|
|
558
|
+
};
|
|
559
|
+
return {
|
|
560
|
+
typeNamesOf: (symbol) => {
|
|
561
|
+
const { byId, nodes } = nodesIn(symbol.position.path);
|
|
562
|
+
const declaration = byId.get(symbol.id);
|
|
563
|
+
return declaration === undefined ? [] : typeNamesOf(declaration, nodes);
|
|
564
|
+
},
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* The names of types one declaration's own syntax writes: in a type reference, a
|
|
570
|
+
* heritage clause or a type query. A function body is not read, and neither is a
|
|
571
|
+
* declaration nested in this one, which `nested` holds and which is read on its own.
|
|
572
|
+
*/
|
|
573
|
+
function typeNamesOf(declaration: Node, nested: ReadonlySet<Node>): readonly Node[] {
|
|
574
|
+
const found: Node[] = [];
|
|
575
|
+
const visit = (node: Node): void => {
|
|
576
|
+
if (node !== declaration && nested.has(node)) {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (isTypeReferenceNode(node)) {
|
|
580
|
+
found.push(isQualifiedName(node.typeName) ? node.typeName.right : node.typeName);
|
|
581
|
+
} else if (isExpressionWithTypeArguments(node)) {
|
|
582
|
+
found.push(
|
|
583
|
+
isPropertyAccessExpression(node.expression) ? node.expression.name : node.expression,
|
|
584
|
+
);
|
|
585
|
+
} else if (isTypeQueryNode(node)) {
|
|
586
|
+
found.push(isQualifiedName(node.exprName) ? node.exprName.right : node.exprName);
|
|
587
|
+
}
|
|
588
|
+
const body = isFunctionLikeDeclaration(node) ? node.body : undefined;
|
|
589
|
+
node.forEachChild((child) => {
|
|
590
|
+
if (child !== body) {
|
|
591
|
+
visit(child);
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
};
|
|
595
|
+
visit(declaration);
|
|
596
|
+
return found;
|
|
440
597
|
}
|
|
441
598
|
|
|
442
599
|
/**
|
package/src/session.ts
CHANGED
|
@@ -485,8 +485,8 @@ function projectsOf(
|
|
|
485
485
|
* snapshot of one client, and answers in their order. A configuration `programs` describes is
|
|
486
486
|
* built as that program, or opened from the configuration it writes, instead of as a project.
|
|
487
487
|
* One the snapshot holds nothing for ends the run under `"refuse"` and is named in the result
|
|
488
|
-
* under `"record"`.
|
|
489
|
-
* after it, whether the visit completed or threw.
|
|
488
|
+
* under `"record"`. `last` runs after the last visit while the client still answers; the
|
|
489
|
+
* snapshot is disposed and the client released after it, whether the visit completed or threw.
|
|
490
490
|
*/
|
|
491
491
|
export function runSession<Result>(
|
|
492
492
|
engine: Engine,
|
|
@@ -494,6 +494,7 @@ export function runSession<Result>(
|
|
|
494
494
|
visit: ProjectVisitor<Result>,
|
|
495
495
|
unopened: Unopened = "refuse",
|
|
496
496
|
programs: ReadonlyMap<string, WorkspaceProgram> = new Map(),
|
|
497
|
+
last: () => void = () => undefined,
|
|
497
498
|
): SessionResult<Result> {
|
|
498
499
|
try {
|
|
499
500
|
const built: string[] = [];
|
|
@@ -531,6 +532,7 @@ export function runSession<Result>(
|
|
|
531
532
|
}
|
|
532
533
|
projects.push(visit(viewOf(project, configFile, engine, log)));
|
|
533
534
|
}
|
|
535
|
+
last();
|
|
534
536
|
return {
|
|
535
537
|
projects,
|
|
536
538
|
unopened: missing,
|
package/src/sweep.ts
CHANGED
|
@@ -166,6 +166,25 @@ export function sweep(graph: Graph, input: SweepInput): Liveness {
|
|
|
166
166
|
return { liveUnder: live, candidates };
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Per declaration of the graph, whether the run that counts test references reaches it.
|
|
171
|
+
* That run's roots are the graph's own and each test file, being the file's top level
|
|
172
|
+
* and every declaration it exports, which a test runner may call; `held` seeds it as a
|
|
173
|
+
* mark or an exemption seeds a sweep.
|
|
174
|
+
*/
|
|
175
|
+
export function reachedWithTheTests(graph: Graph, held: readonly string[]): readonly boolean[] {
|
|
176
|
+
const tests = graph.symbols.flatMap((symbol, at) => {
|
|
177
|
+
const top =
|
|
178
|
+
symbol.kind === "file" || graph.symbols[graph.parent[at] ?? OUTSIDE]?.kind === "file";
|
|
179
|
+
return graph.test[at] === true && top && (symbol.kind === "file" || symbol.exported)
|
|
180
|
+
? [{ at, kind: "test-runner" as const }]
|
|
181
|
+
: [];
|
|
182
|
+
});
|
|
183
|
+
return reachable({ ...graph, rooted: [...graph.rooted, ...tests] }, flagged(graph, held), {
|
|
184
|
+
production: false,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
169
188
|
/**
|
|
170
189
|
* The references a loaded consumer makes that the mode counts. A production mode counts
|
|
171
190
|
* none a consumer's test file made, as it counts none of the target's.
|
package/src/template-field.ts
CHANGED
|
@@ -125,12 +125,74 @@ function stringEnd(text: string, from: number): number {
|
|
|
125
125
|
return at + 1;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
/** The
|
|
129
|
-
function
|
|
128
|
+
/** The end of the `${` substitution whose expression starts at `from`, past its closing brace. */
|
|
129
|
+
function substitutionEnd(text: string, from: number): number {
|
|
130
|
+
let depth = 1;
|
|
131
|
+
let at = from;
|
|
132
|
+
while (at < text.length) {
|
|
133
|
+
const char = text[at];
|
|
134
|
+
if (char === '"' || char === "'") {
|
|
135
|
+
at = stringEnd(text, at);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (char === "`") {
|
|
139
|
+
at = templateEnd(text, at, () => undefined);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (char === "{") {
|
|
143
|
+
depth += 1;
|
|
144
|
+
} else if (char === "}") {
|
|
145
|
+
depth -= 1;
|
|
146
|
+
if (depth === 0) {
|
|
147
|
+
return at + 1;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
at += 1;
|
|
151
|
+
}
|
|
152
|
+
return at;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The end of the template literal at `from`; each `${…}` expression goes to `substitution`. */
|
|
156
|
+
function templateEnd(
|
|
157
|
+
text: string,
|
|
158
|
+
from: number,
|
|
159
|
+
substitution: (expression: string, offset: number) => void,
|
|
160
|
+
): number {
|
|
161
|
+
let at = from + 1;
|
|
162
|
+
while (at < text.length && text[at] !== "`") {
|
|
163
|
+
if (text[at] === "\\") {
|
|
164
|
+
at += 2;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (text[at] === "$" && text[at + 1] === "{") {
|
|
168
|
+
const start = at + 2;
|
|
169
|
+
const end = substitutionEnd(text, start);
|
|
170
|
+
substitution(text.slice(start, end - 1), start);
|
|
171
|
+
at = end;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
at += 1;
|
|
175
|
+
}
|
|
176
|
+
return at + 1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The identifiers one action's inner text writes outside a quoted string, with their offsets
|
|
181
|
+
* in it. In `script` text, a component's markup, a template literal's substitutions are code.
|
|
182
|
+
*/
|
|
183
|
+
function identifiersOf(inner: string, script = false): readonly { name: string; offset: number }[] {
|
|
130
184
|
const found: { name: string; offset: number }[] = [];
|
|
131
185
|
let at = 0;
|
|
132
186
|
while (at < inner.length) {
|
|
133
187
|
const char = inner[at] ?? "";
|
|
188
|
+
if (char === "`" && script) {
|
|
189
|
+
at = templateEnd(inner, at, (expression, offset) => {
|
|
190
|
+
for (const one of identifiersOf(expression, true)) {
|
|
191
|
+
found.push({ name: one.name, offset: offset + one.offset });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
134
196
|
if (char === '"' || char === "'" || char === "`") {
|
|
135
197
|
at = stringEnd(inner, at);
|
|
136
198
|
continue;
|
|
@@ -183,7 +245,7 @@ function namesOf(text: string, delimiters: TemplateDelimiters): readonly Named[]
|
|
|
183
245
|
/** Every identifier the markup of one component file writes in an action. */
|
|
184
246
|
function markupNamesOf(text: string): readonly Named[] {
|
|
185
247
|
return readComponent(text).markup.flatMap((one) =>
|
|
186
|
-
identifiersOf(one.text).map(({ name, offset }) => ({
|
|
248
|
+
identifiersOf(one.text, true).map(({ name, offset }) => ({
|
|
187
249
|
name,
|
|
188
250
|
offset: one.start + offset,
|
|
189
251
|
action: one.action,
|
package/src/test-support.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Test-support code:
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Test-support code: the files only tests reach. A test reaches each one through a chain
|
|
3
|
+
* of references, and every reference to a declaration of one, an import for its effects
|
|
4
|
+
* included, is written in a test file or in another such file, so it is judged with the
|
|
5
|
+
* tests: what it references is referenced by test code.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { InventorySymbol } from "./inventory.ts";
|
|
@@ -13,10 +13,11 @@ import type { Root } from "./roots.ts";
|
|
|
13
13
|
const SKIP_ROOT = "type-error";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* The files of one project that are test-support code, below the target root
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* The files of one project that are test-support code, below the target root: the
|
|
17
|
+
* 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 file. A root has a caller outside the
|
|
19
|
+
* reference graph: a runtime, a tool, a configuration, or a consumer of a library's
|
|
20
|
+
* published API. `testFiles` are the files the test-file rules classified.
|
|
20
21
|
*/
|
|
21
22
|
export function testSupportFiles(
|
|
22
23
|
symbols: readonly InventorySymbol[],
|
|
@@ -26,34 +27,45 @@ export function testSupportFiles(
|
|
|
26
27
|
): ReadonlySet<string> {
|
|
27
28
|
const fileOf = new Map(symbols.map((symbol) => [symbol.id, symbol.position.path]));
|
|
28
29
|
const users = new Map<string, Set<string>>();
|
|
30
|
+
const uses = new Map<string, Set<string>>();
|
|
31
|
+
const add = (map: Map<string, Set<string>>, key: string, value: string): void => {
|
|
32
|
+
const held = map.get(key);
|
|
33
|
+
if (held === undefined) {
|
|
34
|
+
map.set(key, new Set([value]));
|
|
35
|
+
} else {
|
|
36
|
+
held.add(value);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
29
39
|
for (const reference of references) {
|
|
30
40
|
const used = fileOf.get(reference.to);
|
|
31
41
|
const from = reference.position.path;
|
|
32
42
|
if (reference.consumer !== undefined || used === undefined || used === from) {
|
|
33
43
|
continue;
|
|
34
44
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
users.set(used, new Set([from]));
|
|
38
|
-
} else {
|
|
39
|
-
held.add(from);
|
|
40
|
-
}
|
|
45
|
+
add(users, used, from);
|
|
46
|
+
add(uses, from, used);
|
|
41
47
|
}
|
|
42
48
|
const tests = new Set(testFiles);
|
|
43
49
|
const rooted = new Set(
|
|
44
50
|
roots.filter((root) => root.kind !== SKIP_ROOT).flatMap((root) => fileOf.get(root.id) ?? []),
|
|
45
51
|
);
|
|
46
|
-
const
|
|
47
|
-
const
|
|
52
|
+
const reached = new Set<string>();
|
|
53
|
+
const queue = [...tests];
|
|
54
|
+
for (let path = queue.pop(); path !== undefined; path = queue.pop()) {
|
|
55
|
+
for (const next of uses.get(path) ?? []) {
|
|
56
|
+
if (!reached.has(next) && !tests.has(next)) {
|
|
57
|
+
reached.add(next);
|
|
58
|
+
queue.push(next);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const support = new Set([...reached].filter((path) => !rooted.has(path)));
|
|
48
63
|
for (let changed = true; changed;) {
|
|
49
64
|
changed = false;
|
|
50
|
-
for (const path of
|
|
51
|
-
if (support.has(path)) {
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
65
|
+
for (const path of support) {
|
|
54
66
|
const from = users.get(path) ?? new Set<string>();
|
|
55
|
-
if ([...from].every((one) => tests.has(one) || support.has(one))) {
|
|
56
|
-
support.
|
|
67
|
+
if (![...from].every((one) => tests.has(one) || support.has(one))) {
|
|
68
|
+
support.delete(path);
|
|
57
69
|
changed = true;
|
|
58
70
|
}
|
|
59
71
|
}
|
package/src/version.ts
CHANGED