@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/dist/src/roots.js
CHANGED
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
* decides; the published API only supposes a consumer, so a published export is live
|
|
8
8
|
* under reachability and still a candidate under reference counting.
|
|
9
9
|
*/
|
|
10
|
+
import { isExpressionWithTypeArguments, isFunctionLikeDeclaration, isPropertyAccessExpression, isQualifiedName, isTypeQueryNode, isTypeReferenceNode, } from "@typescript/native/unstable/ast";
|
|
10
11
|
import { aliasChains } from "./alias-chain.js";
|
|
11
12
|
import { conventionEntries } from "./conventions.js";
|
|
12
13
|
import { entryPoints } from "./entry-points.js";
|
|
13
14
|
import { globExpression } from "./glob.js";
|
|
14
15
|
import { nodeKey } from "./inventory.js";
|
|
16
|
+
import { relativePath, resolvePath } from "./paths.js";
|
|
15
17
|
import { byPosition } from "./position.js";
|
|
16
18
|
import { UNANSWERED } from "./query.js";
|
|
17
19
|
import { sourceFilesOf } from "./source-files.js";
|
|
@@ -22,6 +24,42 @@ const ENTRY_KINDS = {
|
|
|
22
24
|
run: "manifest-binary",
|
|
23
25
|
script: "script",
|
|
24
26
|
};
|
|
27
|
+
/** The rules whose rooted files a manifest target is read back to by name. */
|
|
28
|
+
const READ_BACK_RULES = new Set([
|
|
29
|
+
"configuration-file",
|
|
30
|
+
"configuration-string",
|
|
31
|
+
"html-entry",
|
|
32
|
+
]);
|
|
33
|
+
/** The extensions a manifest target's name drops: the ones a build writes. */
|
|
34
|
+
const OUTPUT_EXTENSION = /\.(?:d\.ts|d\.mts|d\.cts|js|mjs|cjs|jsx)$/u;
|
|
35
|
+
/** The extension a source file's name drops. */
|
|
36
|
+
const SOURCE_EXTENSION = /\.(?:d\.)?[cm]?[jt]sx?$/u;
|
|
37
|
+
/**
|
|
38
|
+
* The rooted files one manifest target reads back to by name: its path below the
|
|
39
|
+
* manifest's directory, past its first directory where it names one, without its output
|
|
40
|
+
* extension, is the name, and a rooted file is one whose path below the same directory,
|
|
41
|
+
* without its source extension, ends with that name at a directory boundary.
|
|
42
|
+
*/
|
|
43
|
+
function readBackByName(host, entry, rooted) {
|
|
44
|
+
const dir = resolvePath(host.workingDirectory(), entry.dir);
|
|
45
|
+
const below = relativePath(dir, resolvePath(host.workingDirectory(), entry.path));
|
|
46
|
+
if (below === undefined || below === "") {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
const cut = below.indexOf("/");
|
|
50
|
+
const name = (cut === -1 ? below : below.slice(cut + 1)).replace(OUTPUT_EXTENSION, "");
|
|
51
|
+
if (name === "") {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
const found = new Map();
|
|
55
|
+
for (const file of rooted) {
|
|
56
|
+
const path = relativePath(dir, file.fileName)?.replace(SOURCE_EXTENSION, "");
|
|
57
|
+
if (path !== undefined && (path === name || path.endsWith(`/${name}`))) {
|
|
58
|
+
found.set(file.fileName, file);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return [...found.values()];
|
|
62
|
+
}
|
|
25
63
|
/** Whether a configured string carries either special character. */
|
|
26
64
|
function isPattern(text) {
|
|
27
65
|
return text.includes("*") || text.includes("?");
|
|
@@ -124,6 +162,7 @@ export function roots(project, held, targetRoot, options) {
|
|
|
124
162
|
const enter = (file, kind, source, exports) => {
|
|
125
163
|
entered.push({ file, kind, source, exports });
|
|
126
164
|
};
|
|
165
|
+
const points = entryPoints(project, files, options.testFiles, options.host, targetRoot, options.parse);
|
|
127
166
|
const published = [];
|
|
128
167
|
const entries = [
|
|
129
168
|
...options.manifest.entries.map((entry) => ({
|
|
@@ -131,16 +170,27 @@ export function roots(project, held, targetRoot, options) {
|
|
|
131
170
|
publishes: entry.published &&
|
|
132
171
|
(options.manifest.declaresExports ? entry.member.startsWith("exports") : true),
|
|
133
172
|
})),
|
|
134
|
-
...(options.
|
|
173
|
+
...(options.otherPackages ?? []).map((entry) => ({ entry, publishes: entry.published })),
|
|
135
174
|
];
|
|
175
|
+
const scripted = entries.flatMap(({ entry }) => entry.role === "script" ? files.named(entry.path) : []);
|
|
176
|
+
const readBackTo = [
|
|
177
|
+
...scripted,
|
|
178
|
+
...points.filter((point) => READ_BACK_RULES.has(point.rule)).map((point) => point.file),
|
|
179
|
+
];
|
|
180
|
+
const entryFiles = (entry) => {
|
|
181
|
+
const named = files.named(entry.path);
|
|
182
|
+
return named.length > 0 || entry.role === "script"
|
|
183
|
+
? named
|
|
184
|
+
: readBackByName(options.host, entry, readBackTo);
|
|
185
|
+
};
|
|
136
186
|
for (const { entry, publishes } of entries) {
|
|
137
187
|
if (options.publishedAPI && publishes) {
|
|
138
|
-
published.push(...
|
|
188
|
+
published.push(...entryFiles(entry));
|
|
139
189
|
}
|
|
140
190
|
}
|
|
141
191
|
const publishedNames = new Set(published.map((file) => file.fileName));
|
|
142
192
|
for (const { entry } of entries) {
|
|
143
|
-
for (const file of
|
|
193
|
+
for (const file of entryFiles(entry)) {
|
|
144
194
|
const kind = ENTRY_KINDS[entry.role];
|
|
145
195
|
enter(file, kind, entry.member, !publishedNames.has(file.fileName));
|
|
146
196
|
}
|
|
@@ -158,7 +208,7 @@ export function roots(project, held, targetRoot, options) {
|
|
|
158
208
|
: conventionEntries(options.conventions, files)) {
|
|
159
209
|
enter(file, "convention", row, true);
|
|
160
210
|
}
|
|
161
|
-
for (const point of
|
|
211
|
+
for (const point of points) {
|
|
162
212
|
enter(point.file, point.rule, point.source, point.exports);
|
|
163
213
|
}
|
|
164
214
|
for (const file of files.byPath.values()) {
|
|
@@ -188,10 +238,11 @@ export function roots(project, held, targetRoot, options) {
|
|
|
188
238
|
}
|
|
189
239
|
}
|
|
190
240
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
241
|
+
const api = options.publishedAPI
|
|
242
|
+
? publishedAPI(project, published, exportsOf, held, files.byPath)
|
|
243
|
+
: { ids: [], guessed: [] };
|
|
244
|
+
for (const id of api.ids) {
|
|
245
|
+
add(id, "published-api", "");
|
|
195
246
|
}
|
|
196
247
|
const unmatched = configured(held, options.patterns, add);
|
|
197
248
|
const sites = new Map(held.symbols.map((symbol) => [symbol.id, symbol.position]));
|
|
@@ -205,7 +256,7 @@ export function roots(project, held, targetRoot, options) {
|
|
|
205
256
|
configFile: project.configFile,
|
|
206
257
|
liveUnderReachability: ordered,
|
|
207
258
|
unmatched,
|
|
208
|
-
unanswered: answered ?
|
|
259
|
+
unanswered: answered ? api.guessed : held.symbols.map((symbol) => symbol.id),
|
|
209
260
|
};
|
|
210
261
|
}
|
|
211
262
|
/**
|
|
@@ -255,52 +306,134 @@ function exportTables(project, held, wanted) {
|
|
|
255
306
|
/** The kinds of container whose export table, not its members' visibility, says what it publishes. */
|
|
256
307
|
const EXPORTING_CONTAINERS = new Set(["file", "namespace"]);
|
|
257
308
|
/**
|
|
258
|
-
* Every declaration a consumer of the library can name: what the published files
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
* name. Below a published module or namespace the walk follows what its export table
|
|
265
|
-
* names; below any other published declaration it follows every member a consumer
|
|
266
|
-
* can write: a public one, and a protected one, which a consumer's subclass names. A
|
|
267
|
-
* private member is not published, nor is anything below one, and a type parameter
|
|
268
|
-
* is not published either, because a consumer supplies a type argument by position.
|
|
269
|
-
*
|
|
270
|
-
* A published file is not itself published. A consumer imports a module and names
|
|
271
|
-
* what it exports; the file is a root because the manifest names it, which is a
|
|
272
|
-
* different reason.
|
|
309
|
+
* Every declaration a consumer of the library can name: what the published files export
|
|
310
|
+
* and, below each, the members a consumer writes: a namespace's exported ones, any other
|
|
311
|
+
* declaration's public and protected ones, never a private one or a type parameter. A
|
|
312
|
+
* declaration a published one's type names is published too, exported or not. A type name
|
|
313
|
+
* the checker leaves unanswered publishes every declaration spelled as it is, and those are
|
|
314
|
+
* `guessed`. A published file is not itself published.
|
|
273
315
|
*/
|
|
274
|
-
function publishedAPI(published, exportsOf, held) {
|
|
316
|
+
function publishedAPI(project, published, exportsOf, held, byPath) {
|
|
275
317
|
const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
|
|
276
318
|
const children = childrenOf(held);
|
|
319
|
+
const nodes = declarationNodes(held, byPath);
|
|
320
|
+
const chains = aliasChains(project, held);
|
|
277
321
|
const reached = new Set();
|
|
322
|
+
const guessed = new Set();
|
|
278
323
|
const pending = [];
|
|
324
|
+
const reach = (id) => {
|
|
325
|
+
if (!reached.has(id) && byId.get(id)?.kind !== "file") {
|
|
326
|
+
reached.add(id);
|
|
327
|
+
pending.push(id);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
279
330
|
for (const file of published) {
|
|
280
331
|
for (const id of exportsOf.get(file.fileName) ?? []) {
|
|
281
332
|
if (byId.get(id)?.kind === "file") {
|
|
282
333
|
pending.push(id);
|
|
283
334
|
}
|
|
284
|
-
else
|
|
285
|
-
|
|
286
|
-
pending.push(id);
|
|
335
|
+
else {
|
|
336
|
+
reach(id);
|
|
287
337
|
}
|
|
288
338
|
}
|
|
289
339
|
}
|
|
290
|
-
|
|
291
|
-
const
|
|
292
|
-
for (
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
340
|
+
while (pending.length > 0) {
|
|
341
|
+
const round = [];
|
|
342
|
+
for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
|
|
343
|
+
round.push(next);
|
|
344
|
+
const exporting = EXPORTING_CONTAINERS.has(byId.get(next)?.kind ?? "");
|
|
345
|
+
for (const member of children.get(next) ?? []) {
|
|
346
|
+
const reachable = exporting
|
|
347
|
+
? member.exported
|
|
348
|
+
: member.visibility !== "private" && member.visibility !== "private-name";
|
|
349
|
+
if (member.kind !== "type-parameter" && reachable) {
|
|
350
|
+
reach(member.id);
|
|
351
|
+
}
|
|
298
352
|
}
|
|
299
|
-
reached.add(member.id);
|
|
300
|
-
pending.push(member.id);
|
|
301
353
|
}
|
|
354
|
+
const named = round.flatMap((id) => {
|
|
355
|
+
const symbol = byId.get(id);
|
|
356
|
+
return symbol === undefined || symbol.kind === "file" ? [] : nodes.typeNamesOf(symbol);
|
|
357
|
+
});
|
|
358
|
+
project.symbolsAt(named.map((node) => project.handle(node))).forEach((symbol, index) => {
|
|
359
|
+
if (symbol === UNANSWERED) {
|
|
360
|
+
for (const id of held.byName.get(named[index]?.getText() ?? "") ?? []) {
|
|
361
|
+
guessed.add(id);
|
|
362
|
+
reach(id);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
else if (symbol !== undefined) {
|
|
366
|
+
for (const target of chains.chainOf(symbol)) {
|
|
367
|
+
reach(target.id);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
});
|
|
302
371
|
}
|
|
303
|
-
return [...reached];
|
|
372
|
+
return { ids: [...reached], guessed: [...guessed] };
|
|
373
|
+
}
|
|
374
|
+
/** The type names declarations of the inventory write, each file walked once, when first asked. */
|
|
375
|
+
function declarationNodes(held, byPath) {
|
|
376
|
+
const walked = new Map();
|
|
377
|
+
const nodesIn = (path) => {
|
|
378
|
+
const known = walked.get(path);
|
|
379
|
+
if (known !== undefined) {
|
|
380
|
+
return known;
|
|
381
|
+
}
|
|
382
|
+
const found = { byId: new Map(), nodes: new Set() };
|
|
383
|
+
const file = byPath.get(path);
|
|
384
|
+
if (file !== undefined) {
|
|
385
|
+
const visit = (node) => {
|
|
386
|
+
const id = held.declarations.get(nodeKey(file, node));
|
|
387
|
+
if (id !== undefined) {
|
|
388
|
+
found.nodes.add(node);
|
|
389
|
+
if (!found.byId.has(id)) {
|
|
390
|
+
found.byId.set(id, node);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
node.forEachChild(visit);
|
|
394
|
+
};
|
|
395
|
+
visit(file);
|
|
396
|
+
}
|
|
397
|
+
walked.set(path, found);
|
|
398
|
+
return found;
|
|
399
|
+
};
|
|
400
|
+
return {
|
|
401
|
+
typeNamesOf: (symbol) => {
|
|
402
|
+
const { byId, nodes } = nodesIn(symbol.position.path);
|
|
403
|
+
const declaration = byId.get(symbol.id);
|
|
404
|
+
return declaration === undefined ? [] : typeNamesOf(declaration, nodes);
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* The names of types one declaration's own syntax writes: in a type reference, a
|
|
410
|
+
* heritage clause or a type query. A function body is not read, and neither is a
|
|
411
|
+
* declaration nested in this one, which `nested` holds and which is read on its own.
|
|
412
|
+
*/
|
|
413
|
+
function typeNamesOf(declaration, nested) {
|
|
414
|
+
const found = [];
|
|
415
|
+
const visit = (node) => {
|
|
416
|
+
if (node !== declaration && nested.has(node)) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (isTypeReferenceNode(node)) {
|
|
420
|
+
found.push(isQualifiedName(node.typeName) ? node.typeName.right : node.typeName);
|
|
421
|
+
}
|
|
422
|
+
else if (isExpressionWithTypeArguments(node)) {
|
|
423
|
+
found.push(isPropertyAccessExpression(node.expression) ? node.expression.name : node.expression);
|
|
424
|
+
}
|
|
425
|
+
else if (isTypeQueryNode(node)) {
|
|
426
|
+
found.push(isQualifiedName(node.exprName) ? node.exprName.right : node.exprName);
|
|
427
|
+
}
|
|
428
|
+
const body = isFunctionLikeDeclaration(node) ? node.body : undefined;
|
|
429
|
+
node.forEachChild((child) => {
|
|
430
|
+
if (child !== body) {
|
|
431
|
+
visit(child);
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
};
|
|
435
|
+
visit(declaration);
|
|
436
|
+
return found;
|
|
304
437
|
}
|
|
305
438
|
/**
|
|
306
439
|
* Keeps every declaration the configuration names and returns every string that
|
package/dist/src/session.js
CHANGED
|
@@ -225,10 +225,10 @@ function projectsOf(snapshot, built, written) {
|
|
|
225
225
|
* snapshot of one client, and answers in their order. A configuration `programs` describes is
|
|
226
226
|
* built as that program, or opened from the configuration it writes, instead of as a project.
|
|
227
227
|
* One the snapshot holds nothing for ends the run under `"refuse"` and is named in the result
|
|
228
|
-
* under `"record"`.
|
|
229
|
-
* after it, whether the visit completed or threw.
|
|
228
|
+
* under `"record"`. `last` runs after the last visit while the client still answers; the
|
|
229
|
+
* snapshot is disposed and the client released after it, whether the visit completed or threw.
|
|
230
230
|
*/
|
|
231
|
-
export function runSession(engine, configFiles, visit, unopened = "refuse", programs = new Map()) {
|
|
231
|
+
export function runSession(engine, configFiles, visit, unopened = "refuse", programs = new Map(), last = () => undefined) {
|
|
232
232
|
try {
|
|
233
233
|
const built = [];
|
|
234
234
|
const params = [];
|
|
@@ -262,6 +262,7 @@ export function runSession(engine, configFiles, visit, unopened = "refuse", prog
|
|
|
262
262
|
}
|
|
263
263
|
projects.push(visit(viewOf(project, configFile, engine, log)));
|
|
264
264
|
}
|
|
265
|
+
last();
|
|
265
266
|
return {
|
|
266
267
|
projects,
|
|
267
268
|
unopened: missing,
|
package/dist/src/sweep.js
CHANGED
|
@@ -70,6 +70,23 @@ export function sweep(graph, input) {
|
|
|
70
70
|
});
|
|
71
71
|
return { liveUnder: live, candidates };
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Per declaration of the graph, whether the run that counts test references reaches it.
|
|
75
|
+
* That run's roots are the graph's own and each test file, being the file's top level
|
|
76
|
+
* and every declaration it exports, which a test runner may call; `held` seeds it as a
|
|
77
|
+
* mark or an exemption seeds a sweep.
|
|
78
|
+
*/
|
|
79
|
+
export function reachedWithTheTests(graph, held) {
|
|
80
|
+
const tests = graph.symbols.flatMap((symbol, at) => {
|
|
81
|
+
const top = symbol.kind === "file" || graph.symbols[graph.parent[at] ?? OUTSIDE]?.kind === "file";
|
|
82
|
+
return graph.test[at] === true && top && (symbol.kind === "file" || symbol.exported)
|
|
83
|
+
? [{ at, kind: "test-runner" }]
|
|
84
|
+
: [];
|
|
85
|
+
});
|
|
86
|
+
return reachable({ ...graph, rooted: [...graph.rooted, ...tests] }, flagged(graph, held), {
|
|
87
|
+
production: false,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
73
90
|
/**
|
|
74
91
|
* The references a loaded consumer makes that the mode counts. A production mode counts
|
|
75
92
|
* none a consumer's test file made, as it counts none of the target's.
|
|
@@ -87,12 +87,69 @@ function stringEnd(text, from) {
|
|
|
87
87
|
}
|
|
88
88
|
return at + 1;
|
|
89
89
|
}
|
|
90
|
-
/** The
|
|
91
|
-
function
|
|
90
|
+
/** The end of the `${` substitution whose expression starts at `from`, past its closing brace. */
|
|
91
|
+
function substitutionEnd(text, from) {
|
|
92
|
+
let depth = 1;
|
|
93
|
+
let at = from;
|
|
94
|
+
while (at < text.length) {
|
|
95
|
+
const char = text[at];
|
|
96
|
+
if (char === '"' || char === "'") {
|
|
97
|
+
at = stringEnd(text, at);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (char === "`") {
|
|
101
|
+
at = templateEnd(text, at, () => undefined);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (char === "{") {
|
|
105
|
+
depth += 1;
|
|
106
|
+
}
|
|
107
|
+
else if (char === "}") {
|
|
108
|
+
depth -= 1;
|
|
109
|
+
if (depth === 0) {
|
|
110
|
+
return at + 1;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
at += 1;
|
|
114
|
+
}
|
|
115
|
+
return at;
|
|
116
|
+
}
|
|
117
|
+
/** The end of the template literal at `from`; each `${…}` expression goes to `substitution`. */
|
|
118
|
+
function templateEnd(text, from, substitution) {
|
|
119
|
+
let at = from + 1;
|
|
120
|
+
while (at < text.length && text[at] !== "`") {
|
|
121
|
+
if (text[at] === "\\") {
|
|
122
|
+
at += 2;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (text[at] === "$" && text[at + 1] === "{") {
|
|
126
|
+
const start = at + 2;
|
|
127
|
+
const end = substitutionEnd(text, start);
|
|
128
|
+
substitution(text.slice(start, end - 1), start);
|
|
129
|
+
at = end;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
at += 1;
|
|
133
|
+
}
|
|
134
|
+
return at + 1;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The identifiers one action's inner text writes outside a quoted string, with their offsets
|
|
138
|
+
* in it. In `script` text, a component's markup, a template literal's substitutions are code.
|
|
139
|
+
*/
|
|
140
|
+
function identifiersOf(inner, script = false) {
|
|
92
141
|
const found = [];
|
|
93
142
|
let at = 0;
|
|
94
143
|
while (at < inner.length) {
|
|
95
144
|
const char = inner[at] ?? "";
|
|
145
|
+
if (char === "`" && script) {
|
|
146
|
+
at = templateEnd(inner, at, (expression, offset) => {
|
|
147
|
+
for (const one of identifiersOf(expression, true)) {
|
|
148
|
+
found.push({ name: one.name, offset: offset + one.offset });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
96
153
|
if (char === '"' || char === "'" || char === "`") {
|
|
97
154
|
at = stringEnd(inner, at);
|
|
98
155
|
continue;
|
|
@@ -142,7 +199,7 @@ function namesOf(text, delimiters) {
|
|
|
142
199
|
}
|
|
143
200
|
/** Every identifier the markup of one component file writes in an action. */
|
|
144
201
|
function markupNamesOf(text) {
|
|
145
|
-
return readComponent(text).markup.flatMap((one) => identifiersOf(one.text).map(({ name, offset }) => ({
|
|
202
|
+
return readComponent(text).markup.flatMap((one) => identifiersOf(one.text, true).map(({ name, offset }) => ({
|
|
146
203
|
name,
|
|
147
204
|
offset: one.start + offset,
|
|
148
205
|
action: one.action,
|
package/dist/src/test-support.js
CHANGED
|
@@ -1,47 +1,59 @@
|
|
|
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
|
/** The root kind that never takes a file out of test-support code: the skip only withholds. */
|
|
8
8
|
const SKIP_ROOT = "type-error";
|
|
9
9
|
/**
|
|
10
|
-
* The files of one project that are test-support code, below the target root
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* The files of one project that are test-support code, below the target root: the
|
|
11
|
+
* largest set of files a test file reaches, none of them a test file or holding a root,
|
|
12
|
+
* whose every user outside the set is a test file. A root has a caller outside the
|
|
13
|
+
* reference graph: a runtime, a tool, a configuration, or a consumer of a library's
|
|
14
|
+
* published API. `testFiles` are the files the test-file rules classified.
|
|
14
15
|
*/
|
|
15
16
|
export function testSupportFiles(symbols, references, roots, testFiles) {
|
|
16
17
|
const fileOf = new Map(symbols.map((symbol) => [symbol.id, symbol.position.path]));
|
|
17
18
|
const users = new Map();
|
|
19
|
+
const uses = new Map();
|
|
20
|
+
const add = (map, key, value) => {
|
|
21
|
+
const held = map.get(key);
|
|
22
|
+
if (held === undefined) {
|
|
23
|
+
map.set(key, new Set([value]));
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
held.add(value);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
18
29
|
for (const reference of references) {
|
|
19
30
|
const used = fileOf.get(reference.to);
|
|
20
31
|
const from = reference.position.path;
|
|
21
32
|
if (reference.consumer !== undefined || used === undefined || used === from) {
|
|
22
33
|
continue;
|
|
23
34
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
users.set(used, new Set([from]));
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
held.add(from);
|
|
30
|
-
}
|
|
35
|
+
add(users, used, from);
|
|
36
|
+
add(uses, from, used);
|
|
31
37
|
}
|
|
32
38
|
const tests = new Set(testFiles);
|
|
33
39
|
const rooted = new Set(roots.filter((root) => root.kind !== SKIP_ROOT).flatMap((root) => fileOf.get(root.id) ?? []));
|
|
34
|
-
const
|
|
35
|
-
const
|
|
40
|
+
const reached = new Set();
|
|
41
|
+
const queue = [...tests];
|
|
42
|
+
for (let path = queue.pop(); path !== undefined; path = queue.pop()) {
|
|
43
|
+
for (const next of uses.get(path) ?? []) {
|
|
44
|
+
if (!reached.has(next) && !tests.has(next)) {
|
|
45
|
+
reached.add(next);
|
|
46
|
+
queue.push(next);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const support = new Set([...reached].filter((path) => !rooted.has(path)));
|
|
36
51
|
for (let changed = true; changed;) {
|
|
37
52
|
changed = false;
|
|
38
|
-
for (const path of
|
|
39
|
-
if (support.has(path)) {
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
53
|
+
for (const path of support) {
|
|
42
54
|
const from = users.get(path) ?? new Set();
|
|
43
|
-
if ([...from].every((one) => tests.has(one) || support.has(one))) {
|
|
44
|
-
support.
|
|
55
|
+
if (![...from].every((one) => tests.has(one) || support.has(one))) {
|
|
56
|
+
support.delete(path);
|
|
45
57
|
changed = true;
|
|
46
58
|
}
|
|
47
59
|
}
|
package/dist/src/version.js
CHANGED
package/jsr.json
CHANGED