@cplieger/deadset-ts 6.1.1-dev.1 → 7.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 +45 -32
- package/dist/src/component-layer.js +1 -1
- package/dist/src/configuration-files.js +227 -27
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/contextual-uses.js +68 -10
- package/dist/src/convention-rows.js +48 -0
- package/dist/src/conventions.js +80 -0
- package/dist/src/dependencies.js +121 -46
- package/dist/src/discover.js +1 -1
- package/dist/src/edges.js +1 -1
- package/dist/src/exempt-classes.js +9 -22
- package/dist/src/exempt.js +3 -15
- package/dist/src/findings/completion.js +41 -11
- package/dist/src/findings/dependencies-and-module-machinery.js +6 -26
- package/dist/src/findings/emitters.js +0 -4
- package/dist/src/findings/self-check.js +14 -10
- package/dist/src/html-entries.js +29 -14
- package/dist/src/indexed-access.js +377 -81
- package/dist/src/intra-function-parts.js +57 -11
- package/dist/src/inventory.js +3 -12
- package/dist/src/kinds.js +6 -6
- package/dist/src/references.js +62 -23
- package/dist/src/report.js +3 -0
- package/dist/src/roots.js +96 -7
- package/dist/src/run.js +0 -2
- package/dist/src/schema.js +1 -1
- package/dist/src/scope.js +2 -2
- package/dist/src/session.js +6 -2
- package/dist/src/source-files.js +1 -0
- package/dist/src/suppress-inline.js +1 -1
- package/dist/src/suppress.js +15 -16
- 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 -4
- package/dist/src/workspace-resolution.js +0 -2
- package/dist/src/workspace.js +0 -3
- package/jsr.json +1 -1
- package/package.json +2 -1
- package/src/analysis.ts +55 -47
- package/src/calls.ts +3 -3
- package/src/component-files.ts +7 -7
- package/src/component-layer.ts +2 -2
- package/src/components.ts +1 -1
- package/src/config.ts +4 -4
- package/src/configuration-files.ts +246 -28
- package/src/conformance-record.ts +2 -2
- package/src/contextual-uses.ts +99 -13
- package/src/convention-rows.ts +76 -0
- package/src/conventions.ts +102 -0
- package/src/dependencies.ts +148 -56
- package/src/discover.ts +3 -3
- package/src/edges.ts +3 -3
- package/src/entry-points.ts +1 -1
- package/src/exempt-classes.ts +10 -24
- package/src/exempt.ts +5 -29
- package/src/file-facts.ts +1 -1
- package/src/finding.ts +5 -1
- package/src/findings/completion.ts +53 -11
- package/src/findings/dependencies-and-module-machinery.ts +9 -28
- package/src/findings/emitters.ts +3 -8
- package/src/findings/self-check.ts +17 -14
- package/src/findings-pass.ts +2 -2
- package/src/graph.ts +2 -2
- package/src/html-entries.ts +34 -16
- package/src/indexed-access.ts +476 -102
- package/src/intra-function-parts.ts +84 -12
- package/src/inventory.ts +5 -33
- package/src/json-document.ts +1 -1
- package/src/kinds.ts +7 -6
- package/src/manifest.ts +1 -1
- package/src/namespace-objects.ts +1 -1
- package/src/pnpm-workspace.ts +1 -1
- package/src/references.ts +93 -33
- package/src/report.ts +8 -4
- package/src/roots.ts +129 -9
- package/src/run.ts +0 -2
- package/src/schema.ts +2 -2
- package/src/scope.ts +2 -2
- package/src/session.ts +7 -3
- package/src/source-files.ts +3 -0
- package/src/stores.ts +1 -1
- package/src/suppress-inline.ts +3 -3
- package/src/suppress.ts +21 -24
- package/src/sweep.ts +29 -17
- package/src/symbol-keys.ts +172 -0
- package/src/template-field.ts +1 -1
- package/src/test-joins.ts +120 -0
- package/src/value-reach.ts +1 -1
- package/src/verbs/verb.ts +1 -4
- package/src/version.ts +1 -1
- package/src/workflow-steps.ts +23 -16
- package/src/workspace-programs.ts +3 -10
- package/src/workspace-resolution.ts +0 -3
- package/src/workspace-run.ts +1 -1
- package/src/workspace.ts +1 -9
package/src/suppress.ts
CHANGED
|
@@ -65,9 +65,6 @@ export interface Suppressions {
|
|
|
65
65
|
readonly refusals: readonly Refusal[];
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
/** A target that carries no suppression. */
|
|
69
|
-
export const NO_SUPPRESSIONS: Suppressions = { records: [], refusals: [] };
|
|
70
|
-
|
|
71
68
|
/**
|
|
72
69
|
* A directive, an entry or a document the grammar refuses before any finding exists: an
|
|
73
70
|
* instruction the analysis cannot carry out. It is a malformed input, so the command line
|
|
@@ -151,27 +148,26 @@ export interface Dials<F extends Finding = Finding> {
|
|
|
151
148
|
}
|
|
152
149
|
|
|
153
150
|
/** What every record did, and the findings the records withhold. */
|
|
154
|
-
export interface Ledger
|
|
151
|
+
export interface Ledger {
|
|
155
152
|
/** One verdict per record, in reading order. */
|
|
156
153
|
readonly verdicts: readonly Verdict[];
|
|
157
|
-
/** Per record, the finding it claimed, where it claimed one. */
|
|
158
|
-
readonly claims: readonly (F | undefined)[];
|
|
159
154
|
/** Whether a record withholds one finding of the run. */
|
|
160
155
|
readonly withheld: (finding: Finding) => boolean;
|
|
161
156
|
}
|
|
162
157
|
|
|
163
158
|
/**
|
|
164
|
-
* The subject kinds that are a row of a document rather than a declaration of the program
|
|
165
|
-
* A record binds to a declaration, so nothing
|
|
159
|
+
* The subject kinds that are a row of a document rather than a declaration of the program,
|
|
160
|
+
* each with the codes that report one. A record binds to a declaration, so nothing
|
|
161
|
+
* withholds a finding about one.
|
|
166
162
|
*/
|
|
167
|
-
const ROW_SUBJECTS:
|
|
168
|
-
"file",
|
|
169
|
-
"dependency",
|
|
170
|
-
"module-directive",
|
|
171
|
-
"root",
|
|
172
|
-
"configured-declaration",
|
|
173
|
-
"suppression",
|
|
174
|
-
"edge",
|
|
163
|
+
const ROW_SUBJECTS: ReadonlyMap<string, readonly string[]> = new Map([
|
|
164
|
+
["file", ["DS1501", "DS1502"]],
|
|
165
|
+
["dependency", ["DS1601"]],
|
|
166
|
+
["module-directive", ["DS1605"]],
|
|
167
|
+
["root", ["DS1704"]],
|
|
168
|
+
["configured-declaration", ["DS1706"]],
|
|
169
|
+
["suppression", ["DS1701", "DS1702", "DS1703"]],
|
|
170
|
+
["edge", ["DS1705"]],
|
|
175
171
|
]);
|
|
176
172
|
|
|
177
173
|
/** Whether a finding's subject is a row of a document, which no record can withhold. */
|
|
@@ -179,6 +175,11 @@ export function isRowSubject(kind: string): boolean {
|
|
|
179
175
|
return ROW_SUBJECTS.has(kind);
|
|
180
176
|
}
|
|
181
177
|
|
|
178
|
+
/** Whether a code reports a row of a document, so no record naming it can match. */
|
|
179
|
+
export function isRowCode(code: string): boolean {
|
|
180
|
+
return [...ROW_SUBJECTS.values()].some((codes) => codes.includes(code));
|
|
181
|
+
}
|
|
182
|
+
|
|
182
183
|
/**
|
|
183
184
|
* The subject kinds that are a part of a declaration rather than a declaration: a part
|
|
184
185
|
* has no reference of its own, so a finding about one names the declaration that holds
|
|
@@ -242,20 +243,17 @@ export function ledgerOf<F extends Finding>(
|
|
|
242
243
|
records: readonly SuppressionRecord[],
|
|
243
244
|
would: readonly F[],
|
|
244
245
|
dials: Dials<F>,
|
|
245
|
-
): Ledger
|
|
246
|
+
): Ledger {
|
|
246
247
|
const taken = new Set<string>();
|
|
247
248
|
const verdicts: Verdict[] = [];
|
|
248
|
-
const claims: (F | undefined)[] = [];
|
|
249
249
|
for (const record of records) {
|
|
250
250
|
if (record.bound !== "" && dials.withholdsCode(record.code, record.bound)) {
|
|
251
251
|
verdicts.push("dormant");
|
|
252
|
-
claims.push(undefined);
|
|
253
252
|
continue;
|
|
254
253
|
}
|
|
255
254
|
const claimed = would.find(
|
|
256
255
|
(finding) => !taken.has(claimKey(finding)) && pairs(record, finding),
|
|
257
256
|
);
|
|
258
|
-
claims.push(claimed);
|
|
259
257
|
if (claimed === undefined) {
|
|
260
258
|
verdicts.push("stale");
|
|
261
259
|
continue;
|
|
@@ -265,13 +263,12 @@ export function ledgerOf<F extends Finding>(
|
|
|
265
263
|
}
|
|
266
264
|
return {
|
|
267
265
|
verdicts,
|
|
268
|
-
claims,
|
|
269
266
|
withheld: (finding) => taken.has(claimKey(finding)) && !ROW_SUBJECTS.has(finding.symbol.kind),
|
|
270
267
|
};
|
|
271
268
|
}
|
|
272
269
|
|
|
273
270
|
/** The two suppression counts a report's totals carry. */
|
|
274
|
-
|
|
271
|
+
interface SuppressionCounts {
|
|
275
272
|
/** The records in effect. */
|
|
276
273
|
readonly inEffect: number;
|
|
277
274
|
/** The directives, entries and rows that carry a reason, each once whatever its codes. */
|
|
@@ -279,9 +276,9 @@ export interface SuppressionCounts {
|
|
|
279
276
|
}
|
|
280
277
|
|
|
281
278
|
/** The suppression counts of one run's records and the ledger over them. */
|
|
282
|
-
export function suppressionCounts
|
|
279
|
+
export function suppressionCounts(
|
|
283
280
|
records: readonly SuppressionRecord[],
|
|
284
|
-
ledger: Ledger
|
|
281
|
+
ledger: Ledger,
|
|
285
282
|
): SuppressionCounts {
|
|
286
283
|
const sites = new Set(
|
|
287
284
|
records.map((record) => `${record.mechanism}\u0000${positionKey(record.site)}`),
|
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
|
+
}
|
package/src/template-field.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { isAbsolutePath, joinPath, normalizePath, relativePath } from "./paths.t
|
|
|
22
22
|
import { isComponentFile, type Position } from "./position.ts";
|
|
23
23
|
|
|
24
24
|
/** One template, by its path below the target root. */
|
|
25
|
-
|
|
25
|
+
interface TemplateFile {
|
|
26
26
|
readonly path: string;
|
|
27
27
|
readonly text: string;
|
|
28
28
|
}
|
|
@@ -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/value-reach.ts
CHANGED
package/src/verbs/verb.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Config, Inputs } from "../config.ts";
|
|
2
|
-
import type { Emitters } from "../findings/emitter.ts";
|
|
3
2
|
import type { Host } from "../host.ts";
|
|
4
3
|
import type { Writer } from "../run.ts";
|
|
5
4
|
import type { Scope } from "../scope.ts";
|
|
@@ -16,7 +15,7 @@ export const EXIT_FAILURE = 3;
|
|
|
16
15
|
* options that follow its name, read only when the verb asks for what they name, so
|
|
17
16
|
* a verb that asks for nothing refuses no option.
|
|
18
17
|
*/
|
|
19
|
-
|
|
18
|
+
interface Invocation {
|
|
20
19
|
readonly out: Writer;
|
|
21
20
|
readonly err: Writer;
|
|
22
21
|
readonly host: Host;
|
|
@@ -29,8 +28,6 @@ export interface Invocation {
|
|
|
29
28
|
* closed if the verb throws.
|
|
30
29
|
*/
|
|
31
30
|
readonly openClient: (config: Config) => Engine;
|
|
32
|
-
/** The emitter of every kind family, which a verb that reports findings runs. */
|
|
33
|
-
readonly emitters: Emitters;
|
|
34
31
|
/** The arguments after the verb's name, as written. */
|
|
35
32
|
readonly args: readonly string[];
|
|
36
33
|
/** The last value one of the verb's own options was given, undefined where none was. */
|
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;
|
|
@@ -22,12 +22,10 @@ import type { Engine } from "./session.ts";
|
|
|
22
22
|
import type { WorkspaceResolver } from "./workspace-resolution.ts";
|
|
23
23
|
|
|
24
24
|
/** One member import no member source answers for. */
|
|
25
|
-
|
|
25
|
+
interface UnresolvedImport {
|
|
26
26
|
/** The importing file, absolute. */
|
|
27
27
|
readonly file: string;
|
|
28
28
|
readonly specifier: string;
|
|
29
|
-
readonly member: string;
|
|
30
|
-
readonly subpath: string;
|
|
31
29
|
readonly reason: string;
|
|
32
30
|
}
|
|
33
31
|
|
|
@@ -36,8 +34,6 @@ export interface MemberReading {
|
|
|
36
34
|
readonly configFile: string;
|
|
37
35
|
/** The files the configuration itself compiles, absolute. */
|
|
38
36
|
readonly fileNames: readonly string[];
|
|
39
|
-
/** The static resolutions its program is built with; none for a configuration opened as a project. */
|
|
40
|
-
readonly entries: readonly ModuleResolutionEntry[];
|
|
41
37
|
readonly unresolved: readonly UnresolvedImport[];
|
|
42
38
|
/**
|
|
43
39
|
* Why the configuration's component files are not read, where the snapshots read
|
|
@@ -53,7 +49,7 @@ export type WorkspaceProgram =
|
|
|
53
49
|
| { readonly kind: "configuration"; readonly file: string; readonly text: string };
|
|
54
50
|
|
|
55
51
|
/** The programs one run builds instead of opening projects, with what reading them did. */
|
|
56
|
-
|
|
52
|
+
interface WorkspacePrograms {
|
|
57
53
|
/** Per configuration that imports a member, how the snapshot reads it. */
|
|
58
54
|
readonly programs: ReadonlyMap<string, WorkspaceProgram>;
|
|
59
55
|
readonly readings: readonly MemberReading[];
|
|
@@ -126,7 +122,7 @@ function unstatable(reading: Reading): string | undefined {
|
|
|
126
122
|
}
|
|
127
123
|
|
|
128
124
|
/** The path beside `configFile` its written configuration takes, which no file has. */
|
|
129
|
-
function writtenPath(host: Host, configFile: string): string {
|
|
125
|
+
export function writtenPath(host: Host, configFile: string): string {
|
|
130
126
|
const stem = configFile.replace(/\.json$/u, "");
|
|
131
127
|
for (let counter = 0; ; counter += 1) {
|
|
132
128
|
const path = `${stem}.deadset-ts${counter === 0 ? "" : `-${String(counter)}`}.json`;
|
|
@@ -320,8 +316,6 @@ function scan(
|
|
|
320
316
|
reading.unresolved.push({
|
|
321
317
|
file: usage.file,
|
|
322
318
|
specifier,
|
|
323
|
-
member: answer.member,
|
|
324
|
-
subpath: answer.subpath,
|
|
325
319
|
reason: answer.reason,
|
|
326
320
|
});
|
|
327
321
|
}
|
|
@@ -449,7 +443,6 @@ export function workspacePrograms(
|
|
|
449
443
|
readings: readings.map((reading) => ({
|
|
450
444
|
configFile: reading.configFile,
|
|
451
445
|
fileNames: reading.parsed.fileNames,
|
|
452
|
-
entries: reading.entries,
|
|
453
446
|
unresolved: reading.unresolved,
|
|
454
447
|
componentsUnread: unread.get(reading.configFile),
|
|
455
448
|
})),
|
|
@@ -30,7 +30,6 @@ export type MemberResolution =
|
|
|
30
30
|
| {
|
|
31
31
|
readonly kind: "unresolved";
|
|
32
32
|
readonly member: string;
|
|
33
|
-
readonly subpath: string;
|
|
34
33
|
readonly reason: string;
|
|
35
34
|
};
|
|
36
35
|
|
|
@@ -343,7 +342,6 @@ export function workspaceResolver(host: Host, workspace: Workspace): WorkspaceRe
|
|
|
343
342
|
return {
|
|
344
343
|
kind: "unresolved",
|
|
345
344
|
member: name,
|
|
346
|
-
subpath,
|
|
347
345
|
reason: `${String(linked.dirs.length)} workspace packages are named ${name} (${linked.dirs.join(", ")}) and nothing links the importer to one of them; install the workspace, or declare the dependency with a range only one of them satisfies`,
|
|
348
346
|
};
|
|
349
347
|
}
|
|
@@ -381,7 +379,6 @@ export function workspaceResolver(host: Host, workspace: Workspace): WorkspaceRe
|
|
|
381
379
|
return {
|
|
382
380
|
kind: "unresolved",
|
|
383
381
|
member: name,
|
|
384
|
-
subpath,
|
|
385
382
|
reason: built
|
|
386
383
|
? `its manifest names ${named}, output no tsconfig*.json of the package maps back to source, and nothing installed links the importer to it; install the workspace, or point an exports condition the importing configuration's customConditions lists at its source`
|
|
387
384
|
: member.configurations.length === 0
|
package/src/workspace-run.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
import { workspaceResolver } from "./workspace-resolution.ts";
|
|
21
21
|
|
|
22
22
|
/** One run's workspace. */
|
|
23
|
-
|
|
23
|
+
interface WorkspaceRun {
|
|
24
24
|
/** Per configuration that imports a member, how the snapshot reads it. */
|
|
25
25
|
readonly programs: ReadonlyMap<string, WorkspaceProgram>;
|
|
26
26
|
/**
|