@cplieger/deadset-ts 6.1.1-dev.1 → 7.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 +39 -16
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/contextual-uses.js +27 -3
- package/dist/src/convention-rows.js +42 -0
- package/dist/src/conventions.js +48 -0
- package/dist/src/dependencies.js +109 -38
- package/dist/src/findings/completion.js +6 -7
- package/dist/src/findings/dependencies-and-module-machinery.js +6 -26
- package/dist/src/html-entries.js +29 -14
- package/dist/src/indexed-access.js +371 -81
- package/dist/src/intra-function-parts.js +57 -11
- package/dist/src/kinds.js +6 -6
- package/dist/src/references.js +60 -21
- package/dist/src/roots.js +96 -6
- package/dist/src/session.js +6 -1
- package/dist/src/source-files.js +1 -0
- package/dist/src/sweep.js +29 -17
- package/dist/src/symbol-keys.js +114 -0
- package/dist/src/test-joins.js +101 -0
- package/dist/src/version.js +1 -1
- package/dist/src/workflow-steps.js +21 -15
- package/dist/src/workspace-programs.js +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +45 -17
- package/src/conformance-record.ts +2 -2
- package/src/contextual-uses.ts +36 -4
- package/src/convention-rows.ts +57 -0
- package/src/conventions.ts +59 -0
- package/src/dependencies.ts +134 -46
- package/src/finding.ts +2 -0
- package/src/findings/completion.ts +7 -7
- package/src/findings/dependencies-and-module-machinery.ts +9 -28
- package/src/html-entries.ts +33 -15
- package/src/indexed-access.ts +468 -102
- package/src/intra-function-parts.ts +84 -12
- package/src/kinds.ts +7 -6
- package/src/references.ts +86 -27
- package/src/roots.ts +129 -6
- package/src/session.ts +7 -1
- package/src/source-files.ts +3 -0
- package/src/sweep.ts +29 -17
- package/src/symbol-keys.ts +172 -0
- package/src/test-joins.ts +120 -0
- package/src/version.ts +1 -1
- package/src/workflow-steps.ts +23 -16
- package/src/workspace-programs.ts +1 -1
package/src/roots.ts
CHANGED
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
isQualifiedName,
|
|
16
16
|
isTypeQueryNode,
|
|
17
17
|
isTypeReferenceNode,
|
|
18
|
+
isPropertyDeclaration,
|
|
19
|
+
isVariableDeclaration,
|
|
18
20
|
isVariableStatement,
|
|
19
21
|
type Node,
|
|
20
22
|
type SourceFile,
|
|
@@ -31,6 +33,7 @@ import { relativePath, resolvePath } from "./paths.ts";
|
|
|
31
33
|
import { byPosition } from "./position.ts";
|
|
32
34
|
import { UNANSWERED } from "./query.ts";
|
|
33
35
|
import type { ProjectView } from "./session.ts";
|
|
36
|
+
import type { Symbol as TSSymbol, Type } from "@typescript/native/unstable/sync";
|
|
34
37
|
import { sourceFilesOf } from "./source-files.ts";
|
|
35
38
|
import { typeQueryAliases } from "./type-query-alias.ts";
|
|
36
39
|
|
|
@@ -128,6 +131,26 @@ export interface RootOptions {
|
|
|
128
131
|
readonly parse?: ParseFile | undefined;
|
|
129
132
|
/** Whether the target is a library, so a consumer outside it reaches its published API. */
|
|
130
133
|
readonly publishedAPI: boolean;
|
|
134
|
+
/**
|
|
135
|
+
* The directories of the workspace members whose manifest is not private, each a library
|
|
136
|
+
* inside the target whose entries are published API whatever the target's kind.
|
|
137
|
+
*/
|
|
138
|
+
readonly publishedMembers?: ReadonlySet<string> | undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Whether one declaration is written below the directory of a published workspace member. */
|
|
142
|
+
function inPublishedMember(
|
|
143
|
+
held: Inventory,
|
|
144
|
+
id: string,
|
|
145
|
+
root: string,
|
|
146
|
+
members: ReadonlySet<string> | undefined,
|
|
147
|
+
): boolean {
|
|
148
|
+
const path = held.symbols.find((symbol) => symbol.id === id)?.position.path;
|
|
149
|
+
if (path === undefined || members === undefined) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
const file = resolvePath(root, path);
|
|
153
|
+
return [...members].some((dir) => file.startsWith(`${dir}/`));
|
|
131
154
|
}
|
|
132
155
|
|
|
133
156
|
/** The kind of root each role of a manifest entry makes. */
|
|
@@ -305,10 +328,16 @@ export function roots<Brand>(
|
|
|
305
328
|
...options.manifest.entries.map((entry) => ({
|
|
306
329
|
entry,
|
|
307
330
|
publishes:
|
|
331
|
+
options.publishedAPI &&
|
|
308
332
|
entry.published &&
|
|
309
333
|
(options.manifest.declaresExports ? entry.member.startsWith("exports") : true),
|
|
310
334
|
})),
|
|
311
|
-
...(options.otherPackages ?? []).map((entry) => ({
|
|
335
|
+
...(options.otherPackages ?? []).map((entry) => ({
|
|
336
|
+
entry,
|
|
337
|
+
publishes:
|
|
338
|
+
entry.published &&
|
|
339
|
+
(options.publishedAPI || options.publishedMembers?.has(entry.dir) === true),
|
|
340
|
+
})),
|
|
312
341
|
];
|
|
313
342
|
const scripted = entries.flatMap(({ entry }) =>
|
|
314
343
|
entry.role === "script" ? files.named(entry.path) : [],
|
|
@@ -324,7 +353,7 @@ export function roots<Brand>(
|
|
|
324
353
|
: readBackByName(options.host, entry, readBackTo);
|
|
325
354
|
};
|
|
326
355
|
for (const { entry, publishes } of entries) {
|
|
327
|
-
if (
|
|
356
|
+
if (publishes) {
|
|
328
357
|
published.push(...entryFiles(entry));
|
|
329
358
|
}
|
|
330
359
|
}
|
|
@@ -380,9 +409,14 @@ export function roots<Brand>(
|
|
|
380
409
|
}
|
|
381
410
|
}
|
|
382
411
|
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
412
|
+
const tagged = releaseTagged(held, files).filter(
|
|
413
|
+
(id) =>
|
|
414
|
+
options.publishedAPI || inPublishedMember(held, id, targetRoot, options.publishedMembers),
|
|
415
|
+
);
|
|
416
|
+
const api =
|
|
417
|
+
published.length > 0 || tagged.length > 0
|
|
418
|
+
? publishedAPI(project, published, exportsOf, held, files.byPath, tagged)
|
|
419
|
+
: { ids: [], guessed: [] };
|
|
386
420
|
for (const id of api.ids) {
|
|
387
421
|
add(id, "published-api", "");
|
|
388
422
|
}
|
|
@@ -560,6 +594,18 @@ function publishedAPI<Brand>(
|
|
|
560
594
|
const symbol = byId.get(id);
|
|
561
595
|
return symbol === undefined || symbol.kind === "file" ? [] : nodes.typeNamesOf(symbol);
|
|
562
596
|
});
|
|
597
|
+
for (const symbol of inferredTypeSymbols(
|
|
598
|
+
project,
|
|
599
|
+
round.flatMap((id) => {
|
|
600
|
+
const symbol = byId.get(id);
|
|
601
|
+
const node = symbol === undefined ? undefined : nodes.nodeOf(symbol);
|
|
602
|
+
return node === undefined ? [] : [node];
|
|
603
|
+
}),
|
|
604
|
+
)) {
|
|
605
|
+
for (const target of chains.chainOf(symbol)) {
|
|
606
|
+
reach(target.id);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
563
609
|
project.symbolsAt(named.map((node) => project.handle(node))).forEach((symbol, index) => {
|
|
564
610
|
if (symbol === UNANSWERED) {
|
|
565
611
|
for (const id of held.byName.get(named[index]?.getText() ?? "") ?? []) {
|
|
@@ -580,7 +626,10 @@ function publishedAPI<Brand>(
|
|
|
580
626
|
function declarationNodes(
|
|
581
627
|
held: Inventory,
|
|
582
628
|
byPath: ReadonlyMap<string, SourceFile>,
|
|
583
|
-
): {
|
|
629
|
+
): {
|
|
630
|
+
typeNamesOf(symbol: InventorySymbol): readonly Node[];
|
|
631
|
+
nodeOf(symbol: InventorySymbol): Node | undefined;
|
|
632
|
+
} {
|
|
584
633
|
const walked = new Map<string, { byId: Map<string, Node>; nodes: Set<Node> }>();
|
|
585
634
|
const nodesIn = (path: string): { byId: Map<string, Node>; nodes: Set<Node> } => {
|
|
586
635
|
const known = walked.get(path);
|
|
@@ -611,7 +660,81 @@ function declarationNodes(
|
|
|
611
660
|
const declaration = byId.get(symbol.id);
|
|
612
661
|
return declaration === undefined ? [] : typeNamesOf(declaration, nodes);
|
|
613
662
|
},
|
|
663
|
+
nodeOf: (symbol) => nodesIn(symbol.position.path).byId.get(symbol.id),
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/** How deep the type arguments and constituents of an inferred type are followed. */
|
|
668
|
+
const INFERRED_DEPTH = 4;
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* The declarations of the types the compiler infers where a declaration writes none: the
|
|
672
|
+
* type of a variable or a property with no annotation, and the result of a function or a
|
|
673
|
+
* method with no result annotation, each with the types its type arguments and its union
|
|
674
|
+
* or intersection constituents name, to a fixed depth. A question the checker leaves
|
|
675
|
+
* unanswered names nothing.
|
|
676
|
+
*/
|
|
677
|
+
function inferredTypeSymbols<Brand>(
|
|
678
|
+
project: ProjectView<Brand>,
|
|
679
|
+
declarations: readonly Node[],
|
|
680
|
+
): readonly TSSymbol[] {
|
|
681
|
+
const { queries } = project;
|
|
682
|
+
const valued = declarations.flatMap((node) =>
|
|
683
|
+
(isVariableDeclaration(node) || isPropertyDeclaration(node)) &&
|
|
684
|
+
node.type === undefined &&
|
|
685
|
+
!isFunctionLikeDeclaration(node.initializer ?? node)
|
|
686
|
+
? [node.name]
|
|
687
|
+
: [],
|
|
688
|
+
);
|
|
689
|
+
const types: Type[] = [];
|
|
690
|
+
queries.typesAt(valued).forEach((type) => {
|
|
691
|
+
if (type !== UNANSWERED && type !== undefined) {
|
|
692
|
+
types.push(type);
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
for (const node of declarations) {
|
|
696
|
+
const signature = isFunctionLikeDeclaration(node)
|
|
697
|
+
? node
|
|
698
|
+
: isVariableDeclaration(node) &&
|
|
699
|
+
node.type === undefined &&
|
|
700
|
+
node.initializer !== undefined &&
|
|
701
|
+
isFunctionLikeDeclaration(node.initializer)
|
|
702
|
+
? node.initializer
|
|
703
|
+
: undefined;
|
|
704
|
+
if (signature === undefined || signature.type !== undefined || signature.body === undefined) {
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
const typed = queries.signatureOf(signature);
|
|
708
|
+
const result =
|
|
709
|
+
typed === UNANSWERED || typed === undefined ? undefined : queries.returnTypeOf(typed);
|
|
710
|
+
if (result !== UNANSWERED && result !== undefined) {
|
|
711
|
+
types.push(result);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
const found = new Map<number, TSSymbol>();
|
|
715
|
+
const seen = new Set<Type>();
|
|
716
|
+
const visit = (type: Type, depth: number): void => {
|
|
717
|
+
if (depth > INFERRED_DEPTH || seen.has(type)) {
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
seen.add(type);
|
|
721
|
+
const symbol = queries.symbolOfType(type);
|
|
722
|
+
if (symbol !== UNANSWERED && symbol !== undefined) {
|
|
723
|
+
found.set(symbol.id, symbol);
|
|
724
|
+
}
|
|
725
|
+
const parts = queries.constituents(type);
|
|
726
|
+
const args = queries.typeArguments(type);
|
|
727
|
+
for (const next of [
|
|
728
|
+
...(parts === UNANSWERED || (parts.length === 1 && parts[0] === type) ? [] : parts),
|
|
729
|
+
...(args === UNANSWERED ? [] : args),
|
|
730
|
+
]) {
|
|
731
|
+
visit(next, depth + 1);
|
|
732
|
+
}
|
|
614
733
|
};
|
|
734
|
+
types.forEach((type) => {
|
|
735
|
+
visit(type, 0);
|
|
736
|
+
});
|
|
737
|
+
return [...found.values()];
|
|
615
738
|
}
|
|
616
739
|
|
|
617
740
|
/**
|
package/src/session.ts
CHANGED
|
@@ -347,7 +347,13 @@ function viewOf<Brand>(
|
|
|
347
347
|
if ((symbol.flags & SymbolFlags.Alias) === 0) {
|
|
348
348
|
return undefined;
|
|
349
349
|
}
|
|
350
|
-
|
|
350
|
+
// An alias the checker synthesizes declares nothing, the default a module that assigns
|
|
351
|
+
// its export gets under esModuleInterop, and the immediate step fails on it while the
|
|
352
|
+
// whole resolution answers.
|
|
353
|
+
const held =
|
|
354
|
+
symbol.declarations.length === 0
|
|
355
|
+
? queries.aliased(symbol)
|
|
356
|
+
: queries.immediateAliased(symbol);
|
|
351
357
|
// An alias that resolves to nothing answers with the checker's own unknown
|
|
352
358
|
// symbol, which declares nothing; a symbol that is no alias answers with
|
|
353
359
|
// nothing at all.
|
package/src/source-files.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface SourceFiles {
|
|
|
38
38
|
* nothing.
|
|
39
39
|
*/
|
|
40
40
|
named(path: string): readonly SourceFile[];
|
|
41
|
+
/** The directories the project writes its output below, absolute: its output and declaration directories. */
|
|
42
|
+
readonly outputDirs: readonly string[];
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
/**
|
|
@@ -76,6 +78,7 @@ export function sourceFilesOf<Brand>(project: ProjectView<Brand>, targetRoot: st
|
|
|
76
78
|
return {
|
|
77
79
|
byPath,
|
|
78
80
|
byName,
|
|
81
|
+
outputDirs: [...new Set([outDir, declarationDir])].filter((dir) => dir !== undefined),
|
|
79
82
|
named: (path) => {
|
|
80
83
|
if (!path.includes(SUBPATH_WILDCARD)) {
|
|
81
84
|
for (const candidate of candidates(path)) {
|
package/src/sweep.ts
CHANGED
|
@@ -278,34 +278,46 @@ function reachable(graph: Graph, held: readonly boolean[], mode: Mode): readonly
|
|
|
278
278
|
/**
|
|
279
279
|
* Admits every test declaration whose set of referenced production declarations is
|
|
280
280
|
* non-empty and wholly dead, and returns which it admitted. The set is read from every
|
|
281
|
-
* reference the test makes,
|
|
282
|
-
*
|
|
283
|
-
*
|
|
281
|
+
* reference the test makes, which a production sweep does not count. A test-support
|
|
282
|
+
* declaration that a test referencing a live target also references is a live target of
|
|
283
|
+
* every test referencing it. A marked or exempt test is never admitted.
|
|
284
284
|
*/
|
|
285
285
|
function admitTestsOfDeadCode(
|
|
286
286
|
graph: Graph,
|
|
287
287
|
dead: boolean[],
|
|
288
288
|
held: readonly boolean[],
|
|
289
289
|
): readonly boolean[] {
|
|
290
|
-
const
|
|
291
|
-
graph.symbols.
|
|
292
|
-
const testCode = graph.test[at] === true || graph.support[at] === true;
|
|
293
|
-
if (!testCode || graph.subject[at] !== true || held[at] === true) {
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
290
|
+
const testCode = (at: number): boolean => graph.test[at] === true || graph.support[at] === true;
|
|
291
|
+
const judged = graph.symbols.map((_symbol, at) => {
|
|
296
292
|
let targets = 0;
|
|
297
293
|
let live = 0;
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
294
|
+
if (testCode(at) && graph.subject[at] === true && held[at] !== true) {
|
|
295
|
+
for (const edge of graph.out[at] ?? []) {
|
|
296
|
+
if (testCode(edge.to) || graph.subject[edge.to] !== true) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
targets += 1;
|
|
300
|
+
if (dead[edge.to] !== true) {
|
|
301
|
+
live += 1;
|
|
302
|
+
}
|
|
302
303
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
304
|
+
}
|
|
305
|
+
return { targets, live };
|
|
306
|
+
});
|
|
307
|
+
const shared = new Set<number>();
|
|
308
|
+
judged.forEach(({ live }, at) => {
|
|
309
|
+
if (live > 0) {
|
|
310
|
+
for (const edge of graph.out[at] ?? []) {
|
|
311
|
+
if (graph.support[edge.to] === true) {
|
|
312
|
+
shared.add(edge.to);
|
|
313
|
+
}
|
|
306
314
|
}
|
|
307
315
|
}
|
|
308
|
-
|
|
316
|
+
});
|
|
317
|
+
const admitted = graph.symbols.map(() => false);
|
|
318
|
+
judged.forEach(({ targets, live }, at) => {
|
|
319
|
+
const sharing = (graph.out[at] ?? []).some((edge) => shared.has(edge.to) && edge.to !== at);
|
|
320
|
+
if (targets > 0 && live === 0 && !sharing) {
|
|
309
321
|
dead[at] = true;
|
|
310
322
|
admitted[at] = true;
|
|
311
323
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The members a unique symbol keys, used through element accesses. `value[key]`, where
|
|
3
|
+
* `key` is a constant of a `unique symbol` type, reads the member of the receiver's type
|
|
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.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
isComputedPropertyName,
|
|
10
|
+
isIdentifier,
|
|
11
|
+
isNoSubstitutionTemplateLiteral,
|
|
12
|
+
isNumericLiteral,
|
|
13
|
+
isParenthesizedExpression,
|
|
14
|
+
isPropertyAccessExpression,
|
|
15
|
+
isStringLiteral,
|
|
16
|
+
type ElementAccessExpression,
|
|
17
|
+
type Node,
|
|
18
|
+
} from "@typescript/native/unstable/ast";
|
|
19
|
+
import type { Symbol as TSSymbol, Type } from "@typescript/native/unstable/sync";
|
|
20
|
+
import type { AliasChains } from "./alias-chain.ts";
|
|
21
|
+
import type { InventorySymbol } from "./inventory.ts";
|
|
22
|
+
import { isAnswered, type PropertyTables } from "./query.ts";
|
|
23
|
+
import type { ProjectView } from "./session.ts";
|
|
24
|
+
|
|
25
|
+
/** One element access whose key is a name, and how the access uses the member it selects. */
|
|
26
|
+
export interface KeyedAccess {
|
|
27
|
+
readonly access: ElementAccessExpression;
|
|
28
|
+
/** The identifier whose symbol is the key: the argument, or the name its property access ends in. */
|
|
29
|
+
readonly key: Node;
|
|
30
|
+
readonly from: string;
|
|
31
|
+
readonly use: "read" | "write" | "both";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** One keyed access with the symbol its key resolved to. */
|
|
35
|
+
interface ResolvedKey {
|
|
36
|
+
readonly access: KeyedAccess;
|
|
37
|
+
readonly symbol: TSSymbol;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The members one keyed access selects, by their inventory identifiers. */
|
|
41
|
+
interface KeyedUse {
|
|
42
|
+
readonly access: KeyedAccess;
|
|
43
|
+
readonly ids: readonly string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The identifier an element access's argument names its key by, where it is a name. */
|
|
47
|
+
export function keyNameOf(argument: Node): Node | undefined {
|
|
48
|
+
let at = argument;
|
|
49
|
+
while (isParenthesizedExpression(at)) {
|
|
50
|
+
at = at.expression;
|
|
51
|
+
}
|
|
52
|
+
if (isIdentifier(at)) {
|
|
53
|
+
return at;
|
|
54
|
+
}
|
|
55
|
+
return isPropertyAccessExpression(at) && isIdentifier(at.name) ? at.name : undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The last identifier of each computed member name the inventory holds, `mark` for
|
|
60
|
+
* `[mark]` and `iterator` for `[Symbol.iterator]`: an access whose key is spelled by no
|
|
61
|
+
* such identifier selects no member a unique symbol keys, and is not asked about.
|
|
62
|
+
*/
|
|
63
|
+
export function computedKeyNames(symbols: readonly InventorySymbol[]): ReadonlySet<string> {
|
|
64
|
+
const names = new Set<string>();
|
|
65
|
+
for (const symbol of symbols) {
|
|
66
|
+
const key = /\[([^\]]+)\]$/u.exec(symbol.name)?.[1];
|
|
67
|
+
const last = key?.split(".").pop()?.trim();
|
|
68
|
+
if (last !== undefined && /^[\p{ID_Start}$_][\p{ID_Continue}$]*$/u.test(last)) {
|
|
69
|
+
names.add(last);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return names;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Whether a computed name's expression is literal text, which names a member by its text. */
|
|
76
|
+
function isLiteralKey(expression: Node): boolean {
|
|
77
|
+
return (
|
|
78
|
+
isStringLiteral(expression) ||
|
|
79
|
+
isNumericLiteral(expression) ||
|
|
80
|
+
isNoSubstitutionTemplateLiteral(expression)
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The members each keyed access selects. The receivers' types are asked in one batch,
|
|
86
|
+
* each object type's properties once, and the key of every property declared under a
|
|
87
|
+
* computed name in one batch more; a property is selected where its key and the
|
|
88
|
+
* access's key are one symbol.
|
|
89
|
+
*/
|
|
90
|
+
export function keyedUses<Brand>(
|
|
91
|
+
project: ProjectView<Brand>,
|
|
92
|
+
chains: AliasChains,
|
|
93
|
+
resolved: readonly ResolvedKey[],
|
|
94
|
+
properties: PropertyTables,
|
|
95
|
+
cap: number,
|
|
96
|
+
): readonly KeyedUse[] {
|
|
97
|
+
if (resolved.length === 0) {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
const queries = project.queries;
|
|
101
|
+
const ownPaths = project.ownPaths();
|
|
102
|
+
const types = queries.typesAt(
|
|
103
|
+
resolved.map((one) => one.access.access.expression),
|
|
104
|
+
cap,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const parts = new Map<number, readonly Type[]>();
|
|
108
|
+
const partsOf = (type: Type): readonly Type[] => {
|
|
109
|
+
const known = parts.get(type.id);
|
|
110
|
+
if (known !== undefined) {
|
|
111
|
+
return known;
|
|
112
|
+
}
|
|
113
|
+
let found: readonly Type[] = [];
|
|
114
|
+
if (type.isUnionType() || type.isIntersectionType()) {
|
|
115
|
+
const constituents = queries.constituents(type);
|
|
116
|
+
found = isAnswered(constituents) ? constituents.flatMap(partsOf) : [];
|
|
117
|
+
} else if (type.isObjectType()) {
|
|
118
|
+
found = [type];
|
|
119
|
+
}
|
|
120
|
+
parts.set(type.id, found);
|
|
121
|
+
return found;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/** Each receiver's properties declared in the target under a computed name, with that name's expression. */
|
|
125
|
+
const candidates = new Map<number, { property: TSSymbol; key: Node }[]>();
|
|
126
|
+
const keyNodes: Node[] = [];
|
|
127
|
+
const candidatesOf = (type: Type): readonly { property: TSSymbol; key: Node }[] => {
|
|
128
|
+
const known = candidates.get(type.id);
|
|
129
|
+
if (known !== undefined) {
|
|
130
|
+
return known;
|
|
131
|
+
}
|
|
132
|
+
const found: { property: TSSymbol; key: Node }[] = [];
|
|
133
|
+
const read = properties.of(type);
|
|
134
|
+
for (const property of isAnswered(read.properties) ? read.properties : []) {
|
|
135
|
+
const handle = property.declarations.find((one) => ownPaths.has(one.path));
|
|
136
|
+
const node = handle === undefined ? undefined : project.declarationAt(handle)?.node;
|
|
137
|
+
const name = (node as { readonly name?: Node } | undefined)?.name;
|
|
138
|
+
if (name !== undefined && isComputedPropertyName(name) && !isLiteralKey(name.expression)) {
|
|
139
|
+
found.push({ property, key: name.expression });
|
|
140
|
+
keyNodes.push(name.expression);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
candidates.set(type.id, found);
|
|
144
|
+
return found;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const perAccess = resolved.map((_one, index) => {
|
|
148
|
+
const type = types[index];
|
|
149
|
+
return type === undefined || !isAnswered(type) ? [] : partsOf(type).flatMap(candidatesOf);
|
|
150
|
+
});
|
|
151
|
+
const keyOf = new Map<Node, number>();
|
|
152
|
+
const asked = [...new Set(keyNodes)];
|
|
153
|
+
(asked.length === 0
|
|
154
|
+
? []
|
|
155
|
+
: project.symbolsAt(
|
|
156
|
+
asked.map((node) => project.handle(node)),
|
|
157
|
+
cap,
|
|
158
|
+
)
|
|
159
|
+
).forEach((symbol, index) => {
|
|
160
|
+
const node = asked[index];
|
|
161
|
+
if (node !== undefined && symbol !== undefined && isAnswered(symbol)) {
|
|
162
|
+
keyOf.set(node, symbol.id);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return resolved.flatMap((one, index) => {
|
|
167
|
+
const ids = (perAccess[index] ?? [])
|
|
168
|
+
.filter((candidate) => keyOf.get(candidate.key) === one.symbol.id)
|
|
169
|
+
.flatMap((candidate) => chains.declarationsOf(candidate.property));
|
|
170
|
+
return ids.length === 0 ? [] : [{ access: one.access, ids: [...new Set(ids)] }];
|
|
171
|
+
});
|
|
172
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The test files a derived matrix holds although no configuration's file list names them.
|
|
3
|
+
* The test runner compiles such a file with the options of the nearest compiler
|
|
4
|
+
* configuration at or above it, so the file joins that configuration's program as a file
|
|
5
|
+
* its list names, unless a `package.json` below that configuration's directory makes the
|
|
6
|
+
* file another package's, which its own test runner compiles.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { sourcePaths } from "./file-facts.ts";
|
|
10
|
+
import type { Host } from "./host.ts";
|
|
11
|
+
import { dirnamePath, joinPath, relativePath } from "./paths.ts";
|
|
12
|
+
import type { Engine } from "./session.ts";
|
|
13
|
+
import { writtenPath, type WorkspaceProgram } from "./workspace-programs.ts";
|
|
14
|
+
|
|
15
|
+
const PREFERRED = "/tsconfig.json";
|
|
16
|
+
|
|
17
|
+
/** The configurations of one directory in the order a file joins them: `tsconfig.json` first. */
|
|
18
|
+
function byPreference(a: string, b: string): number {
|
|
19
|
+
const first = a.endsWith(PREFERRED) ? 0 : 1;
|
|
20
|
+
const second = b.endsWith(PREFERRED) ? 0 : 1;
|
|
21
|
+
return first - second || (a < b ? -1 : a > b ? 1 : 0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Per configuration of `configFiles`, the absolute paths of the test files that join it:
|
|
26
|
+
* each file below `root` a pattern of `patterns` names, that no configuration's file list
|
|
27
|
+
* holds, joins the configuration nearest at or above its directory when no `package.json`
|
|
28
|
+
* sits between them. A configuration the compiler cannot read lists nothing and is joined
|
|
29
|
+
* by nothing.
|
|
30
|
+
*/
|
|
31
|
+
export function joinedTestFiles(
|
|
32
|
+
engine: Engine,
|
|
33
|
+
host: Host,
|
|
34
|
+
root: string,
|
|
35
|
+
configFiles: readonly string[],
|
|
36
|
+
patterns: readonly RegExp[],
|
|
37
|
+
): ReadonlyMap<string, readonly string[]> {
|
|
38
|
+
const listed = new Set<string>();
|
|
39
|
+
const byDir = new Map<string, string[]>();
|
|
40
|
+
for (const configFile of configFiles) {
|
|
41
|
+
let fileNames: readonly string[];
|
|
42
|
+
try {
|
|
43
|
+
fileNames = engine.parseConfigFile(configFile).fileNames;
|
|
44
|
+
} catch {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
fileNames.forEach((name) => listed.add(name));
|
|
48
|
+
const dir = dirnamePath(configFile);
|
|
49
|
+
byDir.set(dir, [...(byDir.get(dir) ?? []), configFile].sort(byPreference));
|
|
50
|
+
}
|
|
51
|
+
const joins = new Map<string, string[]>();
|
|
52
|
+
for (const path of sourcePaths(host, root)) {
|
|
53
|
+
const file = joinPath(root, path);
|
|
54
|
+
if (listed.has(file) || !patterns.some((pattern) => pattern.test(path))) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
for (let dir = dirnamePath(file); ; dir = dirnamePath(dir)) {
|
|
58
|
+
const [nearest] = byDir.get(dir) ?? [];
|
|
59
|
+
if (nearest !== undefined) {
|
|
60
|
+
joins.set(nearest, [...(joins.get(nearest) ?? []), file]);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
if (
|
|
64
|
+
host.kindOf(joinPath(dir, "package.json")) === "file" ||
|
|
65
|
+
dir === root ||
|
|
66
|
+
relativePath(root, dir) === undefined
|
|
67
|
+
) {
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return joins;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* How the snapshot reads each configuration a test file joins: the program `programs`
|
|
77
|
+
* describes with the files added to its root files, or a configuration only the snapshot
|
|
78
|
+
* holds that extends the one `programs` writes, or the configuration itself, and lists
|
|
79
|
+
* every file the configuration lists and the joined ones.
|
|
80
|
+
*/
|
|
81
|
+
export function withJoinedTests(
|
|
82
|
+
engine: Engine,
|
|
83
|
+
host: Host,
|
|
84
|
+
programs: ReadonlyMap<string, WorkspaceProgram>,
|
|
85
|
+
joins: ReadonlyMap<string, readonly string[]>,
|
|
86
|
+
): ReadonlyMap<string, WorkspaceProgram> {
|
|
87
|
+
const found = new Map(programs);
|
|
88
|
+
for (const [configFile, files] of joins) {
|
|
89
|
+
const program = programs.get(configFile);
|
|
90
|
+
if (program?.kind === "program") {
|
|
91
|
+
found.set(configFile, {
|
|
92
|
+
kind: "program",
|
|
93
|
+
params: {
|
|
94
|
+
...program.params,
|
|
95
|
+
rootFiles: [...(program.params.rootFiles ?? []), ...files],
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
let listed: readonly string[];
|
|
101
|
+
try {
|
|
102
|
+
listed =
|
|
103
|
+
program?.kind === "configuration"
|
|
104
|
+
? engine.parseConfigJson(JSON.parse(program.text), program.file).fileNames
|
|
105
|
+
: engine.parseConfigFile(configFile).fileNames;
|
|
106
|
+
} catch {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const value =
|
|
110
|
+
program?.kind === "configuration"
|
|
111
|
+
? { ...(JSON.parse(program.text) as Record<string, unknown>), files: [...listed, ...files] }
|
|
112
|
+
: { extends: configFile, files: [...listed, ...files] };
|
|
113
|
+
found.set(configFile, {
|
|
114
|
+
kind: "configuration",
|
|
115
|
+
file: program?.kind === "configuration" ? program.file : writtenPath(host, configFile),
|
|
116
|
+
text: `${JSON.stringify({ ...value, include: [] }, null, 2)}\n`,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return found;
|
|
120
|
+
}
|
package/src/version.ts
CHANGED
package/src/workflow-steps.ts
CHANGED
|
@@ -69,6 +69,24 @@ export function runCommands(text: string): readonly string[] {
|
|
|
69
69
|
return found;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/** Every token the `run:` steps of the workflow files in the target root's `.github/workflows` hold. */
|
|
73
|
+
export function workflowTokens(host: Host, targetRoot: string): readonly string[] {
|
|
74
|
+
const dir = joinPath(resolvePath(host.workingDirectory(), targetRoot), WORKFLOWS);
|
|
75
|
+
const found: string[] = [];
|
|
76
|
+
for (const path of workflowFiles(host, dir)) {
|
|
77
|
+
let text: string;
|
|
78
|
+
try {
|
|
79
|
+
text = host.readFile(path);
|
|
80
|
+
} catch {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
for (const command of runCommands(text)) {
|
|
84
|
+
found.push(...scriptTokens(command));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return found;
|
|
88
|
+
}
|
|
89
|
+
|
|
72
90
|
/**
|
|
73
91
|
* The own files the `run:` steps of every workflow file in the target root's
|
|
74
92
|
* `.github/workflows` name: a token equal to a file's path below the target root, with or
|
|
@@ -79,25 +97,14 @@ export function workflowEntries(
|
|
|
79
97
|
targetRoot: string,
|
|
80
98
|
files: SourceFiles,
|
|
81
99
|
): readonly WorkflowEntry[] {
|
|
82
|
-
const dir = joinPath(resolvePath(host.workingDirectory(), targetRoot), WORKFLOWS);
|
|
83
100
|
const found: WorkflowEntry[] = [];
|
|
84
|
-
for (const
|
|
85
|
-
|
|
86
|
-
try {
|
|
87
|
-
text = host.readFile(path);
|
|
88
|
-
} catch {
|
|
101
|
+
for (const token of workflowTokens(host, targetRoot)) {
|
|
102
|
+
if (COMPUTED.test(token)) {
|
|
89
103
|
continue;
|
|
90
104
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
const file = files.byPath.get(token.startsWith("./") ? token.slice(2) : token);
|
|
97
|
-
if (file !== undefined) {
|
|
98
|
-
found.push({ file, source: token });
|
|
99
|
-
}
|
|
100
|
-
}
|
|
105
|
+
const file = files.byPath.get(token.startsWith("./") ? token.slice(2) : token);
|
|
106
|
+
if (file !== undefined) {
|
|
107
|
+
found.push({ file, source: token });
|
|
101
108
|
}
|
|
102
109
|
}
|
|
103
110
|
return found;
|
|
@@ -126,7 +126,7 @@ function unstatable(reading: Reading): string | undefined {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
/** The path beside `configFile` its written configuration takes, which no file has. */
|
|
129
|
-
function writtenPath(host: Host, configFile: string): string {
|
|
129
|
+
export function writtenPath(host: Host, configFile: string): string {
|
|
130
130
|
const stem = configFile.replace(/\.json$/u, "");
|
|
131
131
|
for (let counter = 0; ; counter += 1) {
|
|
132
132
|
const path = `${stem}.deadset-ts${counter === 0 ? "" : `-${String(counter)}`}.json`;
|