@cplieger/deadset-ts 2.2.1 → 3.0.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 +9 -16
- package/dist/src/analysis.js +142 -0
- package/dist/src/components.js +289 -0
- package/dist/src/config.js +6 -0
- package/dist/src/discover.js +98 -24
- package/dist/src/entry-point-gaps.js +278 -0
- package/dist/src/entry-points.js +385 -0
- package/dist/src/finding.js +12 -0
- package/dist/src/glob.js +65 -0
- package/dist/src/graph.js +116 -0
- package/dist/src/index.js +1 -2
- package/dist/src/manifest.js +120 -0
- package/dist/src/print.js +3 -6
- package/dist/src/references.js +156 -106
- package/dist/src/resolve.js +82 -20
- package/dist/src/roots.js +345 -0
- package/dist/src/run.js +59 -10
- package/dist/src/schema.js +1 -1
- package/dist/src/session.js +6 -1
- package/dist/src/source-files.js +122 -0
- package/dist/src/sweep.js +156 -0
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +3 -3
- package/src/analysis.ts +236 -0
- package/src/components.ts +372 -0
- package/src/config.ts +30 -2
- package/src/discover.ts +144 -29
- package/src/entry-point-gaps.ts +304 -0
- package/src/entry-points.ts +496 -0
- package/src/finding.ts +43 -0
- package/src/glob.ts +67 -0
- package/src/graph.ts +178 -0
- package/src/index.ts +1 -20
- package/src/manifest.ts +174 -0
- package/src/print.ts +5 -6
- package/src/references.ts +201 -111
- package/src/resolve.ts +117 -24
- package/src/roots.ts +467 -0
- package/src/run.ts +66 -11
- package/src/schema.ts +5 -2
- package/src/session.ts +6 -1
- package/src/source-files.ts +157 -0
- package/src/sweep.ts +230 -0
- package/src/version.ts +1 -1
package/src/references.ts
CHANGED
|
@@ -3,8 +3,10 @@ import {
|
|
|
3
3
|
isBinaryExpression,
|
|
4
4
|
isDeleteExpression,
|
|
5
5
|
isElementAccessExpression,
|
|
6
|
+
isExportDeclaration,
|
|
6
7
|
isForInStatement,
|
|
7
8
|
isForOfStatement,
|
|
9
|
+
isNamespaceExport,
|
|
8
10
|
isObjectLiteralExpression,
|
|
9
11
|
isParenthesizedExpression,
|
|
10
12
|
isPostfixUnaryExpression,
|
|
@@ -19,6 +21,7 @@ import {
|
|
|
19
21
|
type SourceFile,
|
|
20
22
|
} from "@typescript/native/unstable/ast";
|
|
21
23
|
import { SymbolFlags, type Symbol as TSSymbol } from "@typescript/native/unstable/sync";
|
|
24
|
+
import { globExpression } from "./glob.ts";
|
|
22
25
|
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
23
26
|
import { byPosition, renderPosition, type Position } from "./position.ts";
|
|
24
27
|
import type { ProjectView } from "./session.ts";
|
|
@@ -109,6 +112,12 @@ export interface References {
|
|
|
109
112
|
readonly references: readonly Reference[];
|
|
110
113
|
/** The rules that classified files as test files, ordered by rule. */
|
|
111
114
|
readonly testFileRules: readonly TestFileRule[];
|
|
115
|
+
/**
|
|
116
|
+
* Every file the rules classified as a test file, by its path below the target root,
|
|
117
|
+
* ascending. A declaration such a file holds is a test declaration, by the same
|
|
118
|
+
* classification that marks the references the file makes.
|
|
119
|
+
*/
|
|
120
|
+
readonly testFilePaths: readonly string[];
|
|
112
121
|
readonly cost: ReferenceCost;
|
|
113
122
|
}
|
|
114
123
|
|
|
@@ -272,22 +281,88 @@ interface Site {
|
|
|
272
281
|
readonly use: Use;
|
|
273
282
|
}
|
|
274
283
|
|
|
284
|
+
/**
|
|
285
|
+
* One re-export the walk found, which names what it carries forward without a name
|
|
286
|
+
* that is a use: an export specifier, a namespace export, or a bare star re-export.
|
|
287
|
+
*/
|
|
288
|
+
interface Link {
|
|
289
|
+
/**
|
|
290
|
+
* The node whose symbol says what the re-export carries: the alias a specifier or a
|
|
291
|
+
* namespace export declares, or the module a bare star re-export names.
|
|
292
|
+
*/
|
|
293
|
+
readonly node: Node;
|
|
294
|
+
/** Where the reference is written: the name a specifier re-exports, or the module specifier. */
|
|
295
|
+
readonly at: Node;
|
|
296
|
+
/** The re-export's own declaration, or for a bare star the declaration enclosing it. */
|
|
297
|
+
readonly from: string;
|
|
298
|
+
/** Whether the node's symbol is an alias, which is followed one link along. */
|
|
299
|
+
readonly alias: boolean;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Everything one file's walk found to resolve. */
|
|
303
|
+
interface FileSites {
|
|
304
|
+
readonly uses: readonly Site[];
|
|
305
|
+
readonly links: readonly Link[];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* The re-exports one node writes, if it is an export declaration. A specifier and a
|
|
310
|
+
* namespace export are declarations whose names are where they are written, so no use is
|
|
311
|
+
* read from them; each references what it carries forward instead. A bare star declares
|
|
312
|
+
* nothing, so the declaration enclosing it, at the top level the file, references the
|
|
313
|
+
* module it names.
|
|
314
|
+
*/
|
|
315
|
+
function linksOf(
|
|
316
|
+
file: SourceFile,
|
|
317
|
+
node: Node,
|
|
318
|
+
declarations: ReadonlyMap<string, string>,
|
|
319
|
+
enclosing: string,
|
|
320
|
+
): Link[] {
|
|
321
|
+
if (!isExportDeclaration(node)) {
|
|
322
|
+
return [];
|
|
323
|
+
}
|
|
324
|
+
const clause = node.exportClause;
|
|
325
|
+
const specifier = node.moduleSpecifier;
|
|
326
|
+
if (clause === undefined) {
|
|
327
|
+
return specifier === undefined
|
|
328
|
+
? []
|
|
329
|
+
: [{ node: specifier, at: specifier, from: enclosing, alias: false }];
|
|
330
|
+
}
|
|
331
|
+
if (isNamespaceExport(clause)) {
|
|
332
|
+
const from = declarations.get(nodeKey(file, clause));
|
|
333
|
+
return from === undefined
|
|
334
|
+
? []
|
|
335
|
+
: [{ node: clause.name, at: specifier ?? clause.name, from, alias: true }];
|
|
336
|
+
}
|
|
337
|
+
const found: Link[] = [];
|
|
338
|
+
for (const element of clause.elements) {
|
|
339
|
+
const from = declarations.get(nodeKey(file, element));
|
|
340
|
+
if (from !== undefined) {
|
|
341
|
+
found.push({
|
|
342
|
+
node: element.name,
|
|
343
|
+
at: element.propertyName ?? element.name,
|
|
344
|
+
from,
|
|
345
|
+
alias: true,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return found;
|
|
350
|
+
}
|
|
351
|
+
|
|
275
352
|
/**
|
|
276
353
|
* Every name node one file holds that is a use rather than a declaration, in source
|
|
277
|
-
* order, each with
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
* declared name of the declaration it belongs to is left out: it is where that
|
|
282
|
-
* declaration is written rather than a use of it, which is what makes a declaration
|
|
283
|
-
* referenced only from its own site an unreferenced declaration.
|
|
354
|
+
* order, each with its enclosing declaration and its use, and every re-export the file
|
|
355
|
+
* writes. One walk answers all of it, because a second would have to agree with this one
|
|
356
|
+
* about which nodes are names. A declaration's own declared name is left out, which is
|
|
357
|
+
* what makes a declaration referenced only from its own site an unreferenced one.
|
|
284
358
|
*/
|
|
285
359
|
function sitesOf(
|
|
286
360
|
file: SourceFile,
|
|
287
361
|
declarations: ReadonlyMap<string, string>,
|
|
288
362
|
fileId: string,
|
|
289
|
-
):
|
|
363
|
+
): FileSites {
|
|
290
364
|
const found: Site[] = [];
|
|
365
|
+
const links: Link[] = [];
|
|
291
366
|
const writes = new Set<number>();
|
|
292
367
|
const declared = new Set<number>();
|
|
293
368
|
const shorthands = new Map<number, Node>();
|
|
@@ -325,6 +400,7 @@ function sitesOf(
|
|
|
325
400
|
}
|
|
326
401
|
markNames(node);
|
|
327
402
|
markStores(node, writes);
|
|
403
|
+
links.push(...linksOf(file, node, declarations, enclosing));
|
|
328
404
|
const held = declarations.get(nodeKey(file, node));
|
|
329
405
|
const outer = enclosing;
|
|
330
406
|
if (held !== undefined) {
|
|
@@ -335,65 +411,7 @@ function sitesOf(
|
|
|
335
411
|
};
|
|
336
412
|
|
|
337
413
|
file.forEachChild(visit);
|
|
338
|
-
return found;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* One literal run of a pattern, with every character the expression syntax claims
|
|
343
|
-
* escaped.
|
|
344
|
-
*
|
|
345
|
-
* The set is the syntax characters and the solidus, and it is closed: an escape of any
|
|
346
|
-
* other character is refused outright by the expression syntax under the unicode flag,
|
|
347
|
-
* so escaping a hyphen, which needs none outside a character class, would refuse every
|
|
348
|
-
* pattern that names a hyphenated path.
|
|
349
|
-
*/
|
|
350
|
-
function quoted(text: string): string {
|
|
351
|
-
return text.replaceAll(/[.*+?^${}()|[\]\\/]/gu, (character) => `\\${character}`);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* The expression one glob pattern denotes. `**` spans path separators while `*` and `?`
|
|
356
|
-
* do not, a brace list is an alternation of its members, and every other character
|
|
357
|
-
* stands for itself.
|
|
358
|
-
*/
|
|
359
|
-
function expressionOf(pattern: string): RegExp {
|
|
360
|
-
let source = "^";
|
|
361
|
-
let at = 0;
|
|
362
|
-
while (at < pattern.length) {
|
|
363
|
-
const rest = pattern.slice(at);
|
|
364
|
-
if (rest.startsWith("**/")) {
|
|
365
|
-
source += "(?:[^/]*/)*";
|
|
366
|
-
at += 3;
|
|
367
|
-
continue;
|
|
368
|
-
}
|
|
369
|
-
if (rest.startsWith("**")) {
|
|
370
|
-
source += ".*";
|
|
371
|
-
at += 2;
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
const character = pattern[at] ?? "";
|
|
375
|
-
at += 1;
|
|
376
|
-
if (character === "*") {
|
|
377
|
-
source += "[^/]*";
|
|
378
|
-
continue;
|
|
379
|
-
}
|
|
380
|
-
if (character === "?") {
|
|
381
|
-
source += "[^/]";
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
|
-
if (character === "{") {
|
|
385
|
-
const close = pattern.indexOf("}", at);
|
|
386
|
-
if (close < 0) {
|
|
387
|
-
source += "\\{";
|
|
388
|
-
continue;
|
|
389
|
-
}
|
|
390
|
-
source += `(?:${pattern.slice(at, close).split(",").map(quoted).join("|")})`;
|
|
391
|
-
at = close + 1;
|
|
392
|
-
continue;
|
|
393
|
-
}
|
|
394
|
-
source += quoted(character);
|
|
395
|
-
}
|
|
396
|
-
return new RegExp(`${source}$`, "u");
|
|
414
|
+
return { uses: found, links };
|
|
397
415
|
}
|
|
398
416
|
|
|
399
417
|
/**
|
|
@@ -440,10 +458,12 @@ export function references<Brand>(
|
|
|
440
458
|
): References {
|
|
441
459
|
const cap = options.batchCap ?? DEFAULT_BATCH_CAP;
|
|
442
460
|
const declarations = held.declarations;
|
|
443
|
-
const patterns = options.testFiles.map(
|
|
461
|
+
const patterns = options.testFiles.map(globExpression);
|
|
444
462
|
const matched = options.testFiles.map(() => 0);
|
|
445
463
|
const found: Reference[] = [];
|
|
464
|
+
const tests: string[] = [];
|
|
446
465
|
const reached = new Map<number, readonly Target[]>();
|
|
466
|
+
const steps = new Map<number, TSSymbol | undefined>();
|
|
447
467
|
let batched = 0;
|
|
448
468
|
let fileBatches = 0;
|
|
449
469
|
let residueFallbacks = 0;
|
|
@@ -457,23 +477,53 @@ export function references<Brand>(
|
|
|
457
477
|
const declaredAt = (node: Node): string | undefined =>
|
|
458
478
|
declarations.get(nodeKey(node.getSourceFile(), node));
|
|
459
479
|
|
|
480
|
+
/**
|
|
481
|
+
* The declarations of the inventory one symbol is.
|
|
482
|
+
*
|
|
483
|
+
* A declaration handle carries the file it is in, so the question is answered before
|
|
484
|
+
* the handle is resolved: a declaration outside the project's own files belongs to
|
|
485
|
+
* another program and is never read, which is what keeps the pass from fetching a
|
|
486
|
+
* library file to discover that the inventory does not hold what is in it.
|
|
487
|
+
*/
|
|
488
|
+
const declarationsOf = (symbol: TSSymbol): string[] => {
|
|
489
|
+
const ids: string[] = [];
|
|
490
|
+
for (const handle of symbol.declarations) {
|
|
491
|
+
if (!ownFiles.has(handle.path)) {
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
const node = project.declarationAt(handle)?.node;
|
|
495
|
+
const id = node === undefined ? undefined : declaredAt(node);
|
|
496
|
+
if (id !== undefined && !ids.includes(id)) {
|
|
497
|
+
ids.push(id);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return ids;
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* What one alias names, one link along, asked once per alias whatever the number of
|
|
505
|
+
* chains it is a link of. Only a symbol the binder flagged an alias is asked, which is
|
|
506
|
+
* the condition the step asserts on: a default export of an expression is written in
|
|
507
|
+
* an alias form and names nothing declared elsewhere, so its chain ends at itself.
|
|
508
|
+
*/
|
|
509
|
+
const stepOf = (symbol: TSSymbol): TSSymbol | undefined => {
|
|
510
|
+
if ((symbol.flags & SymbolFlags.Alias) === 0) {
|
|
511
|
+
return undefined;
|
|
512
|
+
}
|
|
513
|
+
if (steps.has(symbol.id)) {
|
|
514
|
+
return steps.get(symbol.id);
|
|
515
|
+
}
|
|
516
|
+
const next = project.aliasStepOf(symbol);
|
|
517
|
+
aliasSteps += 1;
|
|
518
|
+
steps.set(symbol.id, next);
|
|
519
|
+
return next;
|
|
520
|
+
};
|
|
521
|
+
|
|
460
522
|
/**
|
|
461
523
|
* The declarations one symbol names: the declaration it is, and every link of the
|
|
462
524
|
* alias chain it stands at the head of. A re-export something imports is used and so
|
|
463
525
|
* is the declaration behind it, so both are named; a link this project does not
|
|
464
526
|
* declare is stepped over rather than recorded.
|
|
465
|
-
*
|
|
466
|
-
* A symbol is asked to step only where the binder flagged it an alias, which is the
|
|
467
|
-
* same condition the step itself asserts on. The declaration's syntax is not that
|
|
468
|
-
* condition and does not stand in for it: a default export of an expression is
|
|
469
|
-
* written as one of the alias forms and names nothing declared elsewhere, so it is
|
|
470
|
-
* recorded as the declaration it is and the chain ends there.
|
|
471
|
-
*
|
|
472
|
-
* A declaration handle carries the file it is in, so the question the walk asks of a
|
|
473
|
-
* declaration is answered before it is resolved: a declaration outside the project's
|
|
474
|
-
* own files belongs to another program and is never read, which is what keeps the
|
|
475
|
-
* pass from fetching a library file to discover that the inventory does not hold what
|
|
476
|
-
* is in it.
|
|
477
527
|
*/
|
|
478
528
|
const targetsOf = (symbol: TSSymbol): readonly Target[] => {
|
|
479
529
|
const cached = reached.get(symbol.id);
|
|
@@ -486,35 +536,49 @@ export function references<Brand>(
|
|
|
486
536
|
let stepped = false;
|
|
487
537
|
while (at !== undefined && !walked.has(at.id)) {
|
|
488
538
|
walked.add(at.id);
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
if (!ownFiles.has(handle.path)) {
|
|
492
|
-
continue;
|
|
493
|
-
}
|
|
494
|
-
const node = project.declarationAt(handle)?.node;
|
|
495
|
-
const id = node === undefined ? undefined : declaredAt(node);
|
|
496
|
-
if (id !== undefined && !targets.some((target) => target.id === id)) {
|
|
539
|
+
for (const id of declarationsOf(at)) {
|
|
540
|
+
if (!targets.some((target) => target.id === id)) {
|
|
497
541
|
targets.push({ id, stepped });
|
|
498
542
|
}
|
|
499
543
|
}
|
|
500
|
-
|
|
501
|
-
break;
|
|
502
|
-
}
|
|
503
|
-
at = project.aliasStepOf(at);
|
|
504
|
-
aliasSteps += 1;
|
|
544
|
+
at = stepOf(at);
|
|
505
545
|
stepped = true;
|
|
506
546
|
}
|
|
507
547
|
reached.set(symbol.id, targets);
|
|
508
548
|
return targets;
|
|
509
549
|
};
|
|
510
550
|
|
|
551
|
+
/**
|
|
552
|
+
* The declarations one re-export carries forward: for an alias, the first link along
|
|
553
|
+
* its chain that the inventory declares, stepping over the links it does not, such as
|
|
554
|
+
* an import the re-export is written against; for a bare star, the module it names.
|
|
555
|
+
* Each later link is a declaration of its own that references the next, so one link
|
|
556
|
+
* per re-export is what lets a chain be followed one declaration at a time.
|
|
557
|
+
*/
|
|
558
|
+
const carriedBy = (link: Link, symbol: TSSymbol): readonly string[] => {
|
|
559
|
+
if (!link.alias) {
|
|
560
|
+
return declarationsOf(symbol);
|
|
561
|
+
}
|
|
562
|
+
const walked = new Set<number>([symbol.id]);
|
|
563
|
+
let at = stepOf(symbol);
|
|
564
|
+
while (at !== undefined && !walked.has(at.id)) {
|
|
565
|
+
walked.add(at.id);
|
|
566
|
+
const ids = declarationsOf(at);
|
|
567
|
+
if (ids.length > 0) {
|
|
568
|
+
return ids;
|
|
569
|
+
}
|
|
570
|
+
at = stepOf(at);
|
|
571
|
+
}
|
|
572
|
+
return [];
|
|
573
|
+
};
|
|
574
|
+
|
|
511
575
|
/** The symbol one site resolves to, asking only the accessor the site's form names. */
|
|
512
|
-
const symbolFor = (site: Site, answered: ReadonlyMap<
|
|
576
|
+
const symbolFor = (site: Site, answered: ReadonlyMap<Node, TSSymbol>): TSSymbol | undefined => {
|
|
513
577
|
if (site.shorthand !== undefined) {
|
|
514
578
|
shorthandLookups += 1;
|
|
515
579
|
return project.shorthandValueAt(project.handle(site.shorthand));
|
|
516
580
|
}
|
|
517
|
-
const batched = answered.get(site.node
|
|
581
|
+
const batched = answered.get(site.node);
|
|
518
582
|
if (batched !== undefined) {
|
|
519
583
|
return batched;
|
|
520
584
|
}
|
|
@@ -524,38 +588,63 @@ export function references<Brand>(
|
|
|
524
588
|
|
|
525
589
|
for (const file of files) {
|
|
526
590
|
const path = renderPosition(file, targetRoot, 0).path;
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
if (
|
|
591
|
+
const hits = patterns.map((pattern) => pattern.test(path));
|
|
592
|
+
hits.forEach((hit, index) => {
|
|
593
|
+
if (hit) {
|
|
530
594
|
matched[index] = (matched[index] ?? 0) + 1;
|
|
531
|
-
test = true;
|
|
532
595
|
}
|
|
533
596
|
});
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
597
|
+
const test = hits.includes(true);
|
|
598
|
+
if (test) {
|
|
599
|
+
tests.push(path);
|
|
600
|
+
}
|
|
601
|
+
const { uses, links } = sitesOf(file, declarations, declaredAt(file) ?? "");
|
|
602
|
+
|
|
603
|
+
// One batch per capped run of the file's name nodes, the re-exports' nodes after
|
|
604
|
+
// the uses. A shorthand is left out of the batch: its name resolves to the property
|
|
605
|
+
// the literal declares, so the batch's answer for it would name something this
|
|
606
|
+
// project's inventory never holds.
|
|
607
|
+
const batching = [
|
|
608
|
+
...uses.filter((site) => site.shorthand === undefined).map((site) => site.node),
|
|
609
|
+
...links.map((link) => link.node),
|
|
610
|
+
];
|
|
611
|
+
const answered = new Map<Node, TSSymbol>();
|
|
541
612
|
batched += batching.length;
|
|
542
613
|
for (let from = 0; from < batching.length; from += cap) {
|
|
543
614
|
const run = batching.slice(from, from + cap);
|
|
544
|
-
const answers = project.symbolsAt(run.map((
|
|
615
|
+
const answers = project.symbolsAt(run.map((node) => project.handle(node)));
|
|
545
616
|
fileBatches += 1;
|
|
546
|
-
run.forEach((
|
|
617
|
+
run.forEach((node, index) => {
|
|
547
618
|
const symbol = answers[index];
|
|
548
619
|
if (symbol !== undefined) {
|
|
549
|
-
answered.set(
|
|
620
|
+
answered.set(node, symbol);
|
|
550
621
|
}
|
|
551
622
|
});
|
|
552
623
|
}
|
|
553
624
|
|
|
554
|
-
for (const
|
|
625
|
+
for (const link of links) {
|
|
626
|
+
const symbol = answered.get(link.node);
|
|
627
|
+
if (symbol === undefined) {
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
const position = renderPosition(file, targetRoot, link.at.getStart());
|
|
631
|
+
for (const id of carriedBy(link, symbol)) {
|
|
632
|
+
found.push({
|
|
633
|
+
from: link.from,
|
|
634
|
+
to: id,
|
|
635
|
+
position,
|
|
636
|
+
use: "read",
|
|
637
|
+
resolution: link.alias ? "alias" : "batch",
|
|
638
|
+
test,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
for (const site of uses) {
|
|
555
644
|
const direct: Resolution =
|
|
556
645
|
site.shorthand !== undefined
|
|
557
646
|
? "shorthand"
|
|
558
|
-
: answered.has(site.node
|
|
647
|
+
: answered.has(site.node)
|
|
559
648
|
? "batch"
|
|
560
649
|
: "resolved-symbol";
|
|
561
650
|
const symbol = symbolFor(site, answered);
|
|
@@ -590,6 +679,7 @@ export function references<Brand>(
|
|
|
590
679
|
testFileRules: options.testFiles
|
|
591
680
|
.map((_pattern, index) => ({ rule: patternRule(index), matched: matched[index] ?? 0 }))
|
|
592
681
|
.sort((a, b) => compare(a.rule, b.rule)),
|
|
682
|
+
testFilePaths: tests.sort(compare),
|
|
593
683
|
cost: { batched, fileBatches, residueFallbacks, shorthandLookups, aliasSteps },
|
|
594
684
|
};
|
|
595
685
|
}
|
package/src/resolve.ts
CHANGED
|
@@ -38,6 +38,24 @@ const SEVERITY_KEY = /^DS[0-9]{2}([0-9]{2})?$/u;
|
|
|
38
38
|
const EXEMPTION_CLASS = /^[a-z][a-z0-9-]*$/u;
|
|
39
39
|
const CONTRACT_VERSION_PATTERN = /^[0-9]+\.[0-9]+\.[0-9]+$/u;
|
|
40
40
|
|
|
41
|
+
/** The members only a platform build configuration carries. */
|
|
42
|
+
const PLATFORM_MEMBERS = ["os", "arch", "tags"] as const;
|
|
43
|
+
|
|
44
|
+
/** The member only a project build configuration carries. */
|
|
45
|
+
const PROJECT_MEMBER = "project";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* One segment of a project path: not empty, not `.` or `..`, and holding no
|
|
49
|
+
* solidus, backslash or line break.
|
|
50
|
+
*/
|
|
51
|
+
const PROJECT_SEGMENT = String.raw`(?:[^/\\.\r\n][^/\\\r\n]*|\.[^/\\.\r\n][^/\\\r\n]*|\.\.[^/\\\r\n]+)`;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A project path: segments joined by a solidus, so the path is relative, below
|
|
55
|
+
* the root it is read against, and spelled one way on every machine.
|
|
56
|
+
*/
|
|
57
|
+
const PROJECT_PATH = new RegExp(`^${PROJECT_SEGMENT}(?:/${PROJECT_SEGMENT})*$`, "u");
|
|
58
|
+
|
|
41
59
|
const TARGET_KINDS: readonly TargetKind[] = ["application", "library"];
|
|
42
60
|
const CONFIDENCES: readonly Confidence[] = ["certain", "probable", "possible"];
|
|
43
61
|
const GENERATED_FILES: readonly GeneratedFiles[] = ["exclude", "include"];
|
|
@@ -252,10 +270,75 @@ function requireMember(value: string | undefined, path: string, label: string):
|
|
|
252
270
|
return value;
|
|
253
271
|
}
|
|
254
272
|
|
|
273
|
+
/** One build configuration entry as a platform: an identifier, an operating system and an architecture. */
|
|
274
|
+
function readPlatform(
|
|
275
|
+
entry: Record<string, unknown>,
|
|
276
|
+
at: string,
|
|
277
|
+
label: string,
|
|
278
|
+
): BuildConfiguration {
|
|
279
|
+
return {
|
|
280
|
+
shape: "platform",
|
|
281
|
+
id: requireMember(readString(entry, "id", `${at}.id`, label), `${at}.id`, label),
|
|
282
|
+
os: requireMember(readString(entry, "os", `${at}.os`, label), `${at}.os`, label),
|
|
283
|
+
arch: requireMember(readString(entry, "arch", `${at}.arch`, label), `${at}.arch`, label),
|
|
284
|
+
tags: readStrings(entry, "tags", `${at}.tags`, label, 0) ?? [],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** One build configuration entry as a project: an identifier and a configuration file below the target root. */
|
|
289
|
+
function readProject(
|
|
290
|
+
entry: Record<string, unknown>,
|
|
291
|
+
at: string,
|
|
292
|
+
label: string,
|
|
293
|
+
): BuildConfiguration {
|
|
294
|
+
const id = requireMember(readString(entry, "id", `${at}.id`, label), `${at}.id`, label);
|
|
295
|
+
const path = `${at}.${PROJECT_MEMBER}`;
|
|
296
|
+
const project = requireMember(readString(entry, PROJECT_MEMBER, path, label), path, label);
|
|
297
|
+
if (!PROJECT_PATH.test(project)) {
|
|
298
|
+
throw malformed(
|
|
299
|
+
label,
|
|
300
|
+
path,
|
|
301
|
+
`${JSON.stringify(project)} is not a path below the target root: segments joined by /, ` +
|
|
302
|
+
"none empty, . or .., and no backslash or line break",
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
return { shape: "project", id, project };
|
|
306
|
+
}
|
|
307
|
+
|
|
255
308
|
/**
|
|
256
|
-
*
|
|
257
|
-
*
|
|
309
|
+
* One build configuration entry, read as the one shape its members name: a member
|
|
310
|
+
* only a platform carries makes it a platform, the member only a project carries
|
|
311
|
+
* makes it a project, and an entry naming members of both, or of neither, is
|
|
312
|
+
* refused at the entry, because the two shapes are exclusive.
|
|
258
313
|
*/
|
|
314
|
+
function readConfiguration(entry: unknown, at: string, label: string): BuildConfiguration {
|
|
315
|
+
if (!isRecord(entry)) {
|
|
316
|
+
throw malformed(label, at, "is not an object");
|
|
317
|
+
}
|
|
318
|
+
const platform = PLATFORM_MEMBERS.filter((name) => Object.hasOwn(entry, name));
|
|
319
|
+
const project = Object.hasOwn(entry, PROJECT_MEMBER);
|
|
320
|
+
if (platform.length > 0 && project) {
|
|
321
|
+
throw malformed(
|
|
322
|
+
label,
|
|
323
|
+
at,
|
|
324
|
+
`carries ${spell(platform)} of a platform and ${JSON.stringify(PROJECT_MEMBER)} of a ` +
|
|
325
|
+
"project, and an entry is one shape or the other",
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
if (project) {
|
|
329
|
+
return readProject(entry, at, label);
|
|
330
|
+
}
|
|
331
|
+
if (platform.length === 0) {
|
|
332
|
+
throw malformed(
|
|
333
|
+
label,
|
|
334
|
+
at,
|
|
335
|
+
`names neither a platform's "os" and "arch" nor a project's ${JSON.stringify(PROJECT_MEMBER)}`,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
return readPlatform(entry, at, label);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** The build matrix, one entry in one shape at a time. */
|
|
259
342
|
function readConfigurations(
|
|
260
343
|
analysis: Record<string, unknown> | undefined,
|
|
261
344
|
label: string,
|
|
@@ -267,18 +350,9 @@ function readConfigurations(
|
|
|
267
350
|
if (!Array.isArray(value)) {
|
|
268
351
|
throw malformed(label, "analysis.configurations", "is not an array");
|
|
269
352
|
}
|
|
270
|
-
return (value as unknown[]).map((entry, index) =>
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
throw malformed(label, at, "is not an object");
|
|
274
|
-
}
|
|
275
|
-
return {
|
|
276
|
-
id: requireMember(readString(entry, "id", `${at}.id`, label), `${at}.id`, label),
|
|
277
|
-
os: requireMember(readString(entry, "os", `${at}.os`, label), `${at}.os`, label),
|
|
278
|
-
arch: requireMember(readString(entry, "arch", `${at}.arch`, label), `${at}.arch`, label),
|
|
279
|
-
tags: readStrings(entry, "tags", `${at}.tags`, label, 0) ?? [],
|
|
280
|
-
};
|
|
281
|
-
});
|
|
353
|
+
return (value as unknown[]).map((entry, index) =>
|
|
354
|
+
readConfiguration(entry, `analysis.configurations[${String(index)}]`, label),
|
|
355
|
+
);
|
|
282
356
|
}
|
|
283
357
|
|
|
284
358
|
/**
|
|
@@ -642,6 +716,35 @@ function checkLabels(provenance: ReadonlyMap<string, Origin>): void {
|
|
|
642
716
|
* the usage exit code.
|
|
643
717
|
*/
|
|
644
718
|
export function resolve(inputs: Inputs): { config: Config; provenance: Provenance } {
|
|
719
|
+
const { config, provenance } = settleAll(inputs);
|
|
720
|
+
if (config.targetKind === "") {
|
|
721
|
+
throw new ConfigError(
|
|
722
|
+
"missing-target-kind",
|
|
723
|
+
"target.kind",
|
|
724
|
+
"target.kind is not set, it has no default and is never inferred: searched " +
|
|
725
|
+
`${describeSource("the repository configuration", inputs.repository, inputs.repositoryLabel)} and ` +
|
|
726
|
+
describeSource("the central configuration", inputs.central, inputs.centralLabel),
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
checkLabels(provenance);
|
|
730
|
+
return { config, provenance };
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* The build matrix the configuration documents resolve to, every document read and
|
|
735
|
+
* refused as {@link resolve} refuses it, and nothing else required of them: which
|
|
736
|
+
* projects a run analyzes depends on the matrix alone, so a question about the
|
|
737
|
+
* projects asks no target kind.
|
|
738
|
+
*/
|
|
739
|
+
export function resolveMatrix(inputs: Inputs): readonly BuildConfiguration[] {
|
|
740
|
+
return settleAll(inputs).config.analysis.configurations;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Every setting the documents supply, each from the highest-ranked source carrying
|
|
745
|
+
* it, with the origin of each. The target kind is empty where no source names one.
|
|
746
|
+
*/
|
|
747
|
+
function settleAll(inputs: Inputs): { config: Config; provenance: Map<string, Origin> } {
|
|
645
748
|
const sources = readSources(inputs);
|
|
646
749
|
const defaults = defaultConfig();
|
|
647
750
|
const provenance = new Map<string, Origin>();
|
|
@@ -718,15 +821,5 @@ export function resolve(inputs: Inputs): { config: Config; provenance: Provenanc
|
|
|
718
821
|
},
|
|
719
822
|
};
|
|
720
823
|
|
|
721
|
-
if (config.targetKind === "") {
|
|
722
|
-
throw new ConfigError(
|
|
723
|
-
"missing-target-kind",
|
|
724
|
-
"target.kind",
|
|
725
|
-
"target.kind is not set, it has no default and is never inferred: searched " +
|
|
726
|
-
`${describeSource("the repository configuration", inputs.repository, inputs.repositoryLabel)} and ` +
|
|
727
|
-
describeSource("the central configuration", inputs.central, inputs.centralLabel),
|
|
728
|
-
);
|
|
729
|
-
}
|
|
730
|
-
checkLabels(provenance);
|
|
731
824
|
return { config, provenance };
|
|
732
825
|
}
|