@cplieger/deadset-ts 5.1.1 → 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/dist/src/analysis.js +7 -4
- package/dist/src/config.js +3 -0
- package/dist/src/configuration-files.js +23 -24
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/contextual-uses.js +25 -4
- 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/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 +5 -4
- package/dist/src/html-entries.js +137 -0
- package/dist/src/kinds.js +2 -2
- package/dist/src/manifest.js +5 -1
- package/dist/src/matrix.js +9 -3
- package/dist/src/namespace-objects.js +158 -0
- package/dist/src/references.js +120 -3
- package/dist/src/roots.js +52 -3
- package/dist/src/sweep.js +17 -0
- 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 +12 -5
- package/src/config.ts +3 -0
- package/src/configuration-files.ts +28 -24
- package/src/conformance-record.ts +2 -2
- package/src/contextual-uses.ts +31 -4
- 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 +1 -4
- package/src/file-facts.ts +1 -1
- 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 +5 -4
- package/src/html-entries.ts +170 -0
- package/src/kinds.ts +2 -2
- package/src/manifest.ts +9 -3
- package/src/matrix.ts +21 -3
- package/src/namespace-objects.ts +216 -0
- package/src/references.ts +161 -3
- package/src/roots.ts +69 -10
- package/src/sweep.ts +19 -0
- package/src/test-support.ts +34 -22
- package/src/version.ts +1 -1
package/src/contextual-uses.ts
CHANGED
|
@@ -97,12 +97,19 @@ interface Called {
|
|
|
97
97
|
interface Checked {
|
|
98
98
|
readonly source: Type;
|
|
99
99
|
readonly targets: readonly Type[];
|
|
100
|
+
/**
|
|
101
|
+
* Whether the source is declared outside the target, so code the analysis does not
|
|
102
|
+
* hold reads the members of the target's types it declares.
|
|
103
|
+
*/
|
|
104
|
+
readonly outside: boolean;
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
/** One member of an object type, by the inventory's declarations and the checker's declaration handles. */
|
|
103
108
|
interface Member {
|
|
104
109
|
readonly ids: readonly string[];
|
|
105
110
|
readonly handles: ReadonlySet<string>;
|
|
111
|
+
/** Whether a declaration of it is in one of the target's own files. */
|
|
112
|
+
readonly own: boolean;
|
|
106
113
|
}
|
|
107
114
|
|
|
108
115
|
/**
|
|
@@ -120,6 +127,7 @@ export function contextualUses<Brand>(
|
|
|
120
127
|
properties: PropertyTables,
|
|
121
128
|
): ContextualUses {
|
|
122
129
|
const queries = project.queries;
|
|
130
|
+
const ownPaths = project.ownPaths();
|
|
123
131
|
const kinds = new Map(held.symbols.map((symbol) => [symbol.id, symbol.kind]));
|
|
124
132
|
let batches = 0;
|
|
125
133
|
let lookups = 0;
|
|
@@ -162,6 +170,7 @@ export function contextualUses<Brand>(
|
|
|
162
170
|
handles: new Set(
|
|
163
171
|
property.declarations.map((handle) => `${handle.path}#${String(handle.index)}`),
|
|
164
172
|
),
|
|
173
|
+
own: property.declarations.some((handle) => ownPaths.has(handle.path)),
|
|
165
174
|
});
|
|
166
175
|
}
|
|
167
176
|
tables.set(type.id, table);
|
|
@@ -203,6 +212,11 @@ export function contextualUses<Brand>(
|
|
|
203
212
|
member.ids.filter((id) => kinds.get(id) === "type-member");
|
|
204
213
|
const carries = (part: Type): boolean =>
|
|
205
214
|
[...membersOf(part).values()].some((member) => readable(member).length > 0);
|
|
215
|
+
/** Whether an object type has members and declares none of them in the target. */
|
|
216
|
+
const outside = (part: Type): boolean => {
|
|
217
|
+
const members = [...membersOf(part).values()];
|
|
218
|
+
return members.length > 0 && members.every((member) => !member.own);
|
|
219
|
+
};
|
|
206
220
|
|
|
207
221
|
const elements = new Map<number, Type | undefined>();
|
|
208
222
|
/** The element type of an array type, read once per type. */
|
|
@@ -272,7 +286,9 @@ export function contextualUses<Brand>(
|
|
|
272
286
|
const sources = partsOf(source).filter((one) => !targets.some((part) => part.id === one.id));
|
|
273
287
|
for (const one of sources) {
|
|
274
288
|
if (carries(one)) {
|
|
275
|
-
into.push({ source: one, targets });
|
|
289
|
+
into.push({ source: one, targets, outside: false });
|
|
290
|
+
} else if (outside(one)) {
|
|
291
|
+
into.push({ source: one, targets, outside: true });
|
|
276
292
|
}
|
|
277
293
|
const element = elementOf(one);
|
|
278
294
|
const called = element === undefined ? callOf(one) : undefined;
|
|
@@ -303,7 +319,7 @@ export function contextualUses<Brand>(
|
|
|
303
319
|
}
|
|
304
320
|
seen.add(type.id);
|
|
305
321
|
return partsOf(type).some((part) => {
|
|
306
|
-
if (carries(part)) {
|
|
322
|
+
if (carries(part) || outside(part)) {
|
|
307
323
|
return true;
|
|
308
324
|
}
|
|
309
325
|
const element = elementOf(part);
|
|
@@ -332,9 +348,20 @@ export function contextualUses<Brand>(
|
|
|
332
348
|
const checked: Checked[] = [];
|
|
333
349
|
compared(type, position, checked, new Set());
|
|
334
350
|
const ids = new Set<string>();
|
|
335
|
-
for (const { source, targets } of checked) {
|
|
351
|
+
for (const { source, targets, outside: held } of checked) {
|
|
336
352
|
const tables = targets.map(membersOf);
|
|
337
|
-
|
|
353
|
+
const sourceMembers = membersOf(source);
|
|
354
|
+
if (held) {
|
|
355
|
+
for (const table of tables) {
|
|
356
|
+
for (const [name, member] of table) {
|
|
357
|
+
if (sourceMembers.has(name)) {
|
|
358
|
+
member.ids.forEach((id) => ids.add(id));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
for (const [name, member] of sourceMembers) {
|
|
338
365
|
const read = tables.some((table) => {
|
|
339
366
|
const declared = table.get(name);
|
|
340
367
|
return (
|
package/src/convention-rows.ts
CHANGED
|
@@ -25,6 +25,12 @@ interface MoveReading {
|
|
|
25
25
|
readonly call?: OptionsCall;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** One configuration property whose value names a package by a short name. */
|
|
29
|
+
export interface ShortNameReading extends MoveReading {
|
|
30
|
+
/** The package name a value stands for, written with `{}` where the value goes. */
|
|
31
|
+
readonly package: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
28
34
|
/** One directory a row's globs name that a configuration property moves. */
|
|
29
35
|
export interface DirectoryMove {
|
|
30
36
|
/** The placeholder the row's globs and later moves write as `<id>`. */
|
|
@@ -56,6 +62,8 @@ export interface ConventionRow {
|
|
|
56
62
|
readonly generated?: readonly string[];
|
|
57
63
|
/** The directories configuration moves, in the order their placeholders depend on each other. */
|
|
58
64
|
readonly moves: readonly DirectoryMove[];
|
|
65
|
+
/** The properties that name a package the configuration uses by a short name. */
|
|
66
|
+
readonly shortNames?: readonly ShortNameReading[];
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
const SCRIPT = "{js,jsx,ts,tsx}";
|
|
@@ -444,5 +452,12 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
|
|
|
444
452
|
`**/{vitest,vite}.*.config.${ANY_MODULE}`,
|
|
445
453
|
],
|
|
446
454
|
moves: [],
|
|
455
|
+
shortNames: [
|
|
456
|
+
{
|
|
457
|
+
property: "test.coverage.provider",
|
|
458
|
+
files: [`{vitest,vite}.config.${ANY_MODULE}`],
|
|
459
|
+
package: "@vitest/coverage-{}",
|
|
460
|
+
},
|
|
461
|
+
],
|
|
447
462
|
},
|
|
448
463
|
];
|
package/src/conventions.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
|
+
isArrayLiteralExpression,
|
|
10
11
|
isAsExpression,
|
|
11
12
|
isBinaryExpression,
|
|
12
13
|
isCallExpression,
|
|
@@ -50,6 +51,7 @@ import {
|
|
|
50
51
|
type ConventionRow,
|
|
51
52
|
type DirectoryMove,
|
|
52
53
|
type OptionsCall,
|
|
54
|
+
type ShortNameReading,
|
|
53
55
|
} from "./convention-rows.ts";
|
|
54
56
|
import { globExpression } from "./glob.ts";
|
|
55
57
|
import type { Host } from "./host.ts";
|
|
@@ -106,6 +108,8 @@ export interface Conventions {
|
|
|
106
108
|
readonly globs: readonly ConventionGlob[];
|
|
107
109
|
readonly generated: readonly GeneratedDirectory[];
|
|
108
110
|
readonly failures: readonly ConventionFailure[];
|
|
111
|
+
/** The dependencies the applied rows' short names use, by name, each once. */
|
|
112
|
+
readonly uses: readonly string[];
|
|
109
113
|
}
|
|
110
114
|
|
|
111
115
|
/** One file a row roots. */
|
|
@@ -445,6 +449,28 @@ function objectOf(
|
|
|
445
449
|
return at;
|
|
446
450
|
}
|
|
447
451
|
|
|
452
|
+
/** The text of a string literal or a template literal with no substitution, or undefined. */
|
|
453
|
+
function literalText(node: Node): string | undefined {
|
|
454
|
+
const at = unwrapped(node);
|
|
455
|
+
return isStringLiteral(at) || isNoSubstitutionTemplateLiteral(at) ? at.text : undefined;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* The literal texts one value at a path's end writes: the literal itself, or, where
|
|
460
|
+
* `elements`, each element of an array literal of literals. Undefined for any other value.
|
|
461
|
+
*/
|
|
462
|
+
function literalTexts(held: Node, elements: boolean): readonly string[] | undefined {
|
|
463
|
+
const one = literalText(held);
|
|
464
|
+
if (one !== undefined) {
|
|
465
|
+
return [one];
|
|
466
|
+
}
|
|
467
|
+
if (!elements || !isArrayLiteralExpression(held)) {
|
|
468
|
+
return undefined;
|
|
469
|
+
}
|
|
470
|
+
const texts = held.elements.map(literalText);
|
|
471
|
+
return texts.every((text) => text !== undefined) ? texts : undefined;
|
|
472
|
+
}
|
|
473
|
+
|
|
448
474
|
/**
|
|
449
475
|
* Reads the values at one path of keys below one object literal into `values`, and
|
|
450
476
|
* answers the first member that does not write its value out: a spread, a value that is
|
|
@@ -454,6 +480,7 @@ function readPath(
|
|
|
454
480
|
object: ObjectLiteralExpression,
|
|
455
481
|
keys: readonly string[],
|
|
456
482
|
values: string[],
|
|
483
|
+
elements: boolean,
|
|
457
484
|
): Node | undefined {
|
|
458
485
|
const [key, ...rest] = keys;
|
|
459
486
|
for (const member of object.properties) {
|
|
@@ -465,15 +492,16 @@ function readPath(
|
|
|
465
492
|
}
|
|
466
493
|
const held = isPropertyAssignment(member) ? unwrapped(member.initializer) : undefined;
|
|
467
494
|
if (rest.length === 0) {
|
|
468
|
-
|
|
495
|
+
const texts = held === undefined ? undefined : literalTexts(held, elements);
|
|
496
|
+
if (texts === undefined) {
|
|
469
497
|
return member;
|
|
470
498
|
}
|
|
471
|
-
values.push(
|
|
499
|
+
values.push(...texts);
|
|
472
500
|
} else {
|
|
473
501
|
if (held === undefined || !isObjectLiteralExpression(held)) {
|
|
474
502
|
return member;
|
|
475
503
|
}
|
|
476
|
-
const failed = readPath(held, rest, values);
|
|
504
|
+
const failed = readPath(held, rest, values, elements);
|
|
477
505
|
if (failed !== undefined) {
|
|
478
506
|
return failed;
|
|
479
507
|
}
|
|
@@ -493,14 +521,16 @@ interface PropertyReading {
|
|
|
493
521
|
/**
|
|
494
522
|
* Every value one file writes at one property's path inside the object it is read from:
|
|
495
523
|
* the options argument of every call `call` names, or the file's default export. A string
|
|
496
|
-
* literal or a template literal with no substitution is the value
|
|
497
|
-
*
|
|
498
|
-
*
|
|
524
|
+
* literal or a template literal with no substitution is the value, and so is each element
|
|
525
|
+
* of an array literal of them where `elements`. A read object, or an object along the
|
|
526
|
+
* path, that is not written out as an object literal, or that spreads another object into
|
|
527
|
+
* itself, is not a literal, and neither is any other value at the path.
|
|
499
528
|
*/
|
|
500
529
|
export function readProperty(
|
|
501
530
|
file: SourceFile,
|
|
502
531
|
property: string,
|
|
503
532
|
call?: OptionsCall,
|
|
533
|
+
elements = false,
|
|
504
534
|
): PropertyReading {
|
|
505
535
|
const declared = declaredValues(file);
|
|
506
536
|
const objects =
|
|
@@ -515,7 +545,7 @@ export function readProperty(
|
|
|
515
545
|
continue;
|
|
516
546
|
}
|
|
517
547
|
const notLiteral = isObjectLiteralExpression(object)
|
|
518
|
-
? readPath(object, property.split("."), values)
|
|
548
|
+
? readPath(object, property.split("."), values, elements)
|
|
519
549
|
: object;
|
|
520
550
|
if (notLiteral !== undefined) {
|
|
521
551
|
return { values, notLiteral };
|
|
@@ -585,29 +615,19 @@ function resolveMove(
|
|
|
585
615
|
): MoveResult {
|
|
586
616
|
const set: string[] = [];
|
|
587
617
|
for (const reading of move.readings) {
|
|
588
|
-
for (const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
if (read.notLiteral !== undefined) {
|
|
600
|
-
const line = lineOf(text, read.notLiteral.getStart());
|
|
601
|
-
return {
|
|
602
|
-
failure:
|
|
603
|
-
`${name}:${String(line)} sets ${reading.property}, which moves a directory ` +
|
|
604
|
-
`the convention row ${row.name} reads, to a value that is not a string literal: ` +
|
|
605
|
-
`write it as a literal, or name ${row.name} in ts.disabled_conventions and its ` +
|
|
606
|
-
`files in ts.entry_files`,
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
set.push(...read.values);
|
|
618
|
+
for (const { name, text, file } of readingFiles(host, parse, dir, reading.files)) {
|
|
619
|
+
const read = readProperty(file, reading.property, reading.call);
|
|
620
|
+
if (read.notLiteral !== undefined) {
|
|
621
|
+
const line = lineOf(text, read.notLiteral.getStart());
|
|
622
|
+
return {
|
|
623
|
+
failure:
|
|
624
|
+
`${name}:${String(line)} sets ${reading.property}, which moves a directory ` +
|
|
625
|
+
`the convention row ${row.name} reads, to a value that is not a string literal: ` +
|
|
626
|
+
`write it as a literal, or name ${row.name} in ts.disabled_conventions and its ` +
|
|
627
|
+
`files in ts.entry_files`,
|
|
628
|
+
};
|
|
610
629
|
}
|
|
630
|
+
set.push(...read.values);
|
|
611
631
|
}
|
|
612
632
|
}
|
|
613
633
|
const dirs: (string | undefined)[] = [];
|
|
@@ -621,6 +641,71 @@ function resolveMove(
|
|
|
621
641
|
return { dirs: [...new Set(dirs)] };
|
|
622
642
|
}
|
|
623
643
|
|
|
644
|
+
/** The texts each configuration file a reading names holds, parsed, in name order. */
|
|
645
|
+
function readingFiles(
|
|
646
|
+
host: Host,
|
|
647
|
+
parse: ParseSource,
|
|
648
|
+
dir: string,
|
|
649
|
+
files: readonly string[],
|
|
650
|
+
): readonly { readonly name: string; readonly text: string; readonly file: SourceFile }[] {
|
|
651
|
+
const found = [];
|
|
652
|
+
for (const glob of files) {
|
|
653
|
+
for (const name of configurationFiles(host, dir, glob)) {
|
|
654
|
+
const path = joinPath(dir, name);
|
|
655
|
+
let text: string;
|
|
656
|
+
try {
|
|
657
|
+
text = host.readFile(path);
|
|
658
|
+
} catch {
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
found.push({ name, text, file: parse(path, text) });
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return found;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* The dependencies one manifest declares that an applied row's short names use: the
|
|
669
|
+
* package each literal value stands for, and, for a value that is not a literal, every
|
|
670
|
+
* declared dependency the row's package name stands for with some value.
|
|
671
|
+
*/
|
|
672
|
+
function shortNameUses(
|
|
673
|
+
host: Host,
|
|
674
|
+
parse: ParseSource,
|
|
675
|
+
dir: string,
|
|
676
|
+
manifest: unknown,
|
|
677
|
+
readings: readonly ShortNameReading[],
|
|
678
|
+
): readonly string[] {
|
|
679
|
+
const declared = new Set(
|
|
680
|
+
DEPENDENCY_SECTIONS.flatMap((section) => {
|
|
681
|
+
const held = isRecord(manifest) ? manifest[section] : undefined;
|
|
682
|
+
return isRecord(held) ? Object.keys(held) : [];
|
|
683
|
+
}),
|
|
684
|
+
);
|
|
685
|
+
const used = new Set<string>();
|
|
686
|
+
for (const reading of readings) {
|
|
687
|
+
const [before = "", after = ""] = reading.package.split("{}");
|
|
688
|
+
for (const { file } of readingFiles(host, parse, dir, reading.files)) {
|
|
689
|
+
const read = readProperty(file, reading.property, reading.call, true);
|
|
690
|
+
for (const value of read.values) {
|
|
691
|
+
used.add(`${before}${value}${after}`);
|
|
692
|
+
}
|
|
693
|
+
if (read.notLiteral !== undefined) {
|
|
694
|
+
for (const name of declared) {
|
|
695
|
+
if (
|
|
696
|
+
name.length >= before.length + after.length &&
|
|
697
|
+
name.startsWith(before) &&
|
|
698
|
+
name.endsWith(after)
|
|
699
|
+
) {
|
|
700
|
+
used.add(name);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return [...used].filter((name) => declared.has(name)).sort(compare);
|
|
707
|
+
}
|
|
708
|
+
|
|
624
709
|
/** Two strings ordered bytewise. */
|
|
625
710
|
function compare(a: string, b: string): number {
|
|
626
711
|
if (a === b) {
|
|
@@ -644,6 +729,7 @@ export function readConventions(
|
|
|
644
729
|
const globs: ConventionGlob[] = [];
|
|
645
730
|
const generated: GeneratedDirectory[] = [];
|
|
646
731
|
const failures: ConventionFailure[] = [];
|
|
732
|
+
const uses = new Set<string>();
|
|
647
733
|
for (const manifest of manifests) {
|
|
648
734
|
const declared = readJSON(host, joinPath(manifest.dir, "package.json"));
|
|
649
735
|
const versions = new Map<string, string | undefined>();
|
|
@@ -702,6 +788,9 @@ export function readConventions(
|
|
|
702
788
|
version,
|
|
703
789
|
manifest: manifest.path,
|
|
704
790
|
});
|
|
791
|
+
for (const name of shortNameUses(host, parse, manifest.dir, declared, row.shortNames ?? [])) {
|
|
792
|
+
uses.add(name);
|
|
793
|
+
}
|
|
705
794
|
const excludes = [
|
|
706
795
|
...new Set((row.excludes ?? []).flatMap((one) => expand(one, resolved))),
|
|
707
796
|
].map(globExpression);
|
|
@@ -725,7 +814,7 @@ export function readConventions(
|
|
|
725
814
|
compare(a.manifest, b.manifest) ||
|
|
726
815
|
compare(JSON.stringify(a), JSON.stringify(b)),
|
|
727
816
|
);
|
|
728
|
-
return { applied: unique, globs, generated, failures };
|
|
817
|
+
return { applied: unique, globs, generated, failures, uses: [...uses].sort(compare) };
|
|
729
818
|
}
|
|
730
819
|
|
|
731
820
|
/**
|
package/src/dependencies.ts
CHANGED
|
@@ -61,6 +61,15 @@ export interface DeclaredDependency {
|
|
|
61
61
|
readonly position: FindingPosition;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** What a configuration's own reading needs: packages no deletion of a declaration removes. */
|
|
65
|
+
export function configurationNeeds(packages: readonly string[]): ProjectNeeds {
|
|
66
|
+
return {
|
|
67
|
+
packages: new Set(packages),
|
|
68
|
+
uses: new Map([[PROJECT_OWNER, new Set(packages)]]),
|
|
69
|
+
unanswered: false,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
64
73
|
/** What the installed copy of one declared dependency states about itself. */
|
|
65
74
|
export interface InstalledDependency {
|
|
66
75
|
/** Whether its manifest declares a command. */
|
package/src/entry-point-gaps.ts
CHANGED
|
@@ -26,10 +26,10 @@ const UNREAD =
|
|
|
26
26
|
"file only it loads is reported as never imported unless ts.entry_files or roots.patterns " +
|
|
27
27
|
"names it.";
|
|
28
28
|
|
|
29
|
-
/** Why a configuration's file names that are not
|
|
30
|
-
const
|
|
31
|
-
"A string of a configuration file names a file only where it is a literal
|
|
32
|
-
"
|
|
29
|
+
/** Why a configuration's file names that are not literals are declined. */
|
|
30
|
+
const NOT_LITERAL =
|
|
31
|
+
"A string of a configuration file names a file only where it is a literal, so a file a " +
|
|
32
|
+
"computed value names is reported as never imported.";
|
|
33
33
|
|
|
34
34
|
/** The tools whose own conventions are not read. */
|
|
35
35
|
const UNREAD_TOOLS: readonly string[] = [
|
|
@@ -230,11 +230,6 @@ const PARTLY_ENTERED: readonly DeclinedConvention[] = [
|
|
|
230
230
|
"A testMatch that is not a string literal leaves the test-file set unknown, and an " +
|
|
231
231
|
"unknown set enters nothing.",
|
|
232
232
|
},
|
|
233
|
-
{
|
|
234
|
-
tool: "playwright",
|
|
235
|
-
convention: "a globalSetup or globalTeardown file named without a leading ./ or ../",
|
|
236
|
-
reason: NOT_RELATIVE,
|
|
237
|
-
},
|
|
238
233
|
{
|
|
239
234
|
tool: "playwright",
|
|
240
235
|
convention: "the testDir and testMatch of an entry in projects",
|
|
@@ -300,22 +295,15 @@ const PARTLY_ENTERED: readonly DeclinedConvention[] = [
|
|
|
300
295
|
},
|
|
301
296
|
{
|
|
302
297
|
tool: "vite",
|
|
303
|
-
convention: "an entry point its configuration
|
|
304
|
-
reason:
|
|
305
|
-
},
|
|
306
|
-
{
|
|
307
|
-
tool: "vite",
|
|
308
|
-
convention: "the module scripts an index.html loads",
|
|
309
|
-
reason:
|
|
310
|
-
"No rule reads an HTML file, so a script only index.html loads is reported as never " +
|
|
311
|
-
"imported unless ts.entry_files names it.",
|
|
298
|
+
convention: "an entry point its configuration computes",
|
|
299
|
+
reason: NOT_LITERAL,
|
|
312
300
|
},
|
|
313
301
|
{
|
|
314
302
|
tool: "vitest",
|
|
315
|
-
convention: "the benchmark
|
|
303
|
+
convention: "the benchmark files",
|
|
316
304
|
reason:
|
|
317
|
-
"A test file is one ts.test_files classifies, so a *.bench.*
|
|
318
|
-
"
|
|
305
|
+
"A test file is one ts.test_files classifies, so a *.bench.* file is entered only " +
|
|
306
|
+
"where a configured pattern names it.",
|
|
319
307
|
},
|
|
320
308
|
{
|
|
321
309
|
tool: "vitest",
|
|
@@ -324,11 +312,6 @@ const PARTLY_ENTERED: readonly DeclinedConvention[] = [
|
|
|
324
312
|
"A string is read against the configuration file's directory, so a setup file named " +
|
|
325
313
|
"against a root the configuration moves is not entered.",
|
|
326
314
|
},
|
|
327
|
-
{
|
|
328
|
-
tool: "vitest",
|
|
329
|
-
convention: "a setup file named without a leading ./ or ../",
|
|
330
|
-
reason: NOT_RELATIVE,
|
|
331
|
-
},
|
|
332
315
|
{
|
|
333
316
|
tool: "vitest",
|
|
334
317
|
convention: "the test files a configuration's include member names",
|
|
@@ -338,8 +321,8 @@ const PARTLY_ENTERED: readonly DeclinedConvention[] = [
|
|
|
338
321
|
},
|
|
339
322
|
{
|
|
340
323
|
tool: "webpack",
|
|
341
|
-
convention: "an entry point its configuration
|
|
342
|
-
reason:
|
|
324
|
+
convention: "an entry point its configuration computes",
|
|
325
|
+
reason: NOT_LITERAL,
|
|
343
326
|
},
|
|
344
327
|
];
|
|
345
328
|
|
package/src/entry-points.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
} from "./configuration-files.ts";
|
|
39
39
|
import { globExpression } from "./glob.ts";
|
|
40
40
|
import type { Host } from "./host.ts";
|
|
41
|
+
import { htmlEntries } from "./html-entries.ts";
|
|
41
42
|
import { dirnamePath, relativePath, resolvePath } from "./paths.ts";
|
|
42
43
|
import { UNANSWERED } from "./query.ts";
|
|
43
44
|
import type { ProjectView } from "./session.ts";
|
|
@@ -56,7 +57,9 @@ export type EntryRule =
|
|
|
56
57
|
/** The test files below the browser-test runner's test directory. */
|
|
57
58
|
| "browser-tests"
|
|
58
59
|
/** A worker or service worker a call addresses by a string literal. */
|
|
59
|
-
| "worker"
|
|
60
|
+
| "worker"
|
|
61
|
+
/** A file a module script of an HTML file below the target root loads. */
|
|
62
|
+
| "html-entry";
|
|
60
63
|
|
|
61
64
|
/** One file a rule enters. */
|
|
62
65
|
export interface EntryPoint {
|
|
@@ -542,5 +545,8 @@ export function entryPoints<Brand>(
|
|
|
542
545
|
}
|
|
543
546
|
found.push(...workersOf(project, file, files));
|
|
544
547
|
}
|
|
548
|
+
for (const { file, source } of htmlEntries(host, targetRoot, files, parse)) {
|
|
549
|
+
found.push({ file, rule: "html-entry", source, exports: true });
|
|
550
|
+
}
|
|
545
551
|
return found;
|
|
546
552
|
}
|
package/src/exempt.ts
CHANGED
|
@@ -48,10 +48,7 @@ export interface DetectorInput<Brand> {
|
|
|
48
48
|
readonly ts: TSSection;
|
|
49
49
|
/** The absolute path of each declared consumer, whose modules are part of the analysis. */
|
|
50
50
|
readonly consumers: readonly string[];
|
|
51
|
-
/**
|
|
52
|
-
* The project's generated files, by path below the target root, each with the
|
|
53
|
-
* convention row that names its directory as generated.
|
|
54
|
-
*/
|
|
51
|
+
/** The project's generated files, by path below the target root, each with the detail its retentions name. */
|
|
55
52
|
readonly generated?: ReadonlyMap<string, string>;
|
|
56
53
|
}
|
|
57
54
|
|
package/src/file-facts.ts
CHANGED
|
@@ -47,7 +47,7 @@ export interface FileFacts {
|
|
|
47
47
|
* reference names is included by the file that names it.
|
|
48
48
|
*/
|
|
49
49
|
readonly heldByInclusion: ReadonlySet<string>;
|
|
50
|
-
/** The paths below the target root of the files
|
|
50
|
+
/** The paths below the target root of the generated files. */
|
|
51
51
|
readonly generated?: ReadonlySet<string>;
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -55,16 +55,19 @@ function publishedIds(union: Graph): ReadonlySet<string> {
|
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
57
|
* The reachability class of one declaration of the run. A declaration of a library's
|
|
58
|
-
* published API, a public member of a published type included,
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* in the loaded program, a private member and a `#private` name among them, and is
|
|
64
|
-
* `certain`.
|
|
58
|
+
* published API, a public member of a published type included, is `certain` where the
|
|
59
|
+
* run loaded every consumer the scope declared and declared one, `probable` where one did
|
|
60
|
+
* not load, and `possible` where it declared none, whatever the configuration says of the
|
|
61
|
+
* consumer set. A declaration of test-support code that test code references, outside the
|
|
62
|
+
* test-of-dead-code rule, is `possible`. Every other declaration is `certain`.
|
|
65
63
|
*/
|
|
66
64
|
function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
|
|
67
65
|
const published = publishedIds(input.swept.matrix.union);
|
|
66
|
+
const usedByTests = new Set(
|
|
67
|
+
input.swept.sweep.candidates
|
|
68
|
+
.filter((candidate) => candidate.heldByTests === candidate.id)
|
|
69
|
+
.map((candidate) => candidate.id),
|
|
70
|
+
);
|
|
68
71
|
const { declared } = input.boundary.consumers;
|
|
69
72
|
const outside: Confidence =
|
|
70
73
|
declared.length === 0
|
|
@@ -72,7 +75,12 @@ function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
|
|
|
72
75
|
: everyDeclaredLoaded(input.boundary)
|
|
73
76
|
? "certain"
|
|
74
77
|
: "probable";
|
|
75
|
-
return (id) =>
|
|
78
|
+
return (id) => {
|
|
79
|
+
if (usedByTests.has(id)) {
|
|
80
|
+
return "possible";
|
|
81
|
+
}
|
|
82
|
+
return published.has(id) ? outside : "certain";
|
|
83
|
+
};
|
|
76
84
|
}
|
|
77
85
|
|
|
78
86
|
/**
|
|
@@ -147,6 +155,12 @@ export function completed(
|
|
|
147
155
|
const componentOf = findingComponents(swept, input.config.reporters.cascade);
|
|
148
156
|
let minted = swept.sweep.components.length;
|
|
149
157
|
const dead = new Set(swept.sweep.components.map((component) => component.id));
|
|
158
|
+
const fallingWith = new Map<string, number>();
|
|
159
|
+
for (const { heldByTests } of swept.sweep.candidates) {
|
|
160
|
+
if (heldByTests !== undefined) {
|
|
161
|
+
fallingWith.set(heldByTests, (fallingWith.get(heldByTests) ?? 0) + 1);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
150
164
|
|
|
151
165
|
const done = findings.map((finding): CompletedFinding => {
|
|
152
166
|
const row = kinds.get(finding.code);
|
|
@@ -165,8 +179,14 @@ export function completed(
|
|
|
165
179
|
const judged = !part && union.subject[at] === true ? symbol : undefined;
|
|
166
180
|
const candidate = judged === undefined ? undefined : candidates.get(judged.id);
|
|
167
181
|
const found = symbol === undefined || part ? undefined : componentOf(symbol.id);
|
|
168
|
-
// A test file's declaration is a component member only as a test of dead code
|
|
169
|
-
|
|
182
|
+
// A test file's declaration is a component member only as a test of dead code, and
|
|
183
|
+
// test code keeps the test-support code it references out of every component.
|
|
184
|
+
if (
|
|
185
|
+
candidate !== undefined &&
|
|
186
|
+
found === undefined &&
|
|
187
|
+
union.test[at] !== true &&
|
|
188
|
+
candidate.heldByTests === undefined
|
|
189
|
+
) {
|
|
170
190
|
throw new Error(`${candidate.id} is judged dead and falls in no dead component`);
|
|
171
191
|
}
|
|
172
192
|
let component = found;
|
|
@@ -175,7 +195,7 @@ export function completed(
|
|
|
175
195
|
component = {
|
|
176
196
|
id: componentId(minted),
|
|
177
197
|
root: true,
|
|
178
|
-
symbolCount: 1,
|
|
198
|
+
symbolCount: symbol?.kind === "interface" ? (fallingWith.get(symbol.id) ?? 1) : 1,
|
|
179
199
|
deletableLines: 0,
|
|
180
200
|
};
|
|
181
201
|
}
|
|
@@ -31,7 +31,6 @@ function compare(a: string, b: string): number {
|
|
|
31
31
|
function indexed(input: EmitterInput): {
|
|
32
32
|
readonly symbolOf: (id: string) => InventorySymbol | undefined;
|
|
33
33
|
readonly inTest: (id: string) => boolean;
|
|
34
|
-
readonly inSupport: (id: string) => boolean;
|
|
35
34
|
readonly candidateOf: (id: string) => Candidate | undefined;
|
|
36
35
|
readonly implementationsOf: (id: string) => readonly PositionedSymbol[];
|
|
37
36
|
} {
|
|
@@ -44,7 +43,6 @@ function indexed(input: EmitterInput): {
|
|
|
44
43
|
return {
|
|
45
44
|
symbolOf,
|
|
46
45
|
inTest: (id) => union.test[union.at(id)] === true,
|
|
47
|
-
inSupport: (id) => union.support[union.at(id)] === true,
|
|
48
46
|
candidateOf: (id) => candidates.get(id),
|
|
49
47
|
implementationsOf: (id) =>
|
|
50
48
|
[...(input.implementations.classes.get(id) ?? [])]
|
|
@@ -82,19 +80,14 @@ function findingOf(
|
|
|
82
80
|
* interface, whose members fall with it unreported; and `DS1203`, a method of a live
|
|
83
81
|
* interface no reference names, unless it is a marker, a method whose every
|
|
84
82
|
* implementation has an empty body. A test file's declaration is neither kind's
|
|
85
|
-
* subject, because a production sweep counts none of the references it can have
|
|
86
|
-
* neither is a test-support declaration test code references, which that code keeps live.
|
|
83
|
+
* subject, because a production sweep counts none of the references it can have.
|
|
87
84
|
*/
|
|
88
85
|
export const interfaces: Emitter = (input) => {
|
|
89
86
|
const run = indexed(input);
|
|
90
87
|
const found: Finding[] = [];
|
|
91
88
|
for (const candidate of input.swept.sweep.candidates) {
|
|
92
89
|
const symbol = run.symbolOf(candidate.id);
|
|
93
|
-
if (
|
|
94
|
-
symbol === undefined ||
|
|
95
|
-
run.inTest(candidate.id) ||
|
|
96
|
-
(run.inSupport(candidate.id) && candidate.testRefs > 0)
|
|
97
|
-
) {
|
|
90
|
+
if (symbol === undefined || run.inTest(candidate.id)) {
|
|
98
91
|
continue;
|
|
99
92
|
}
|
|
100
93
|
if (symbol.kind === "interface") {
|