@cplieger/deadset-ts 5.0.0 → 5.1.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 +77 -10
- package/dist/src/configuration-files.js +107 -3
- package/dist/src/conformance-record.js +3 -9
- package/dist/src/contextual-uses.js +240 -0
- package/dist/src/convention-rows.js +6 -0
- package/dist/src/conventions.js +123 -14
- package/dist/src/dependencies.js +8 -2
- package/dist/src/destructuring.js +6 -6
- package/dist/src/entry-points.js +8 -5
- package/dist/src/exempt-classes.js +6 -1
- package/dist/src/exempt.js +21 -17
- package/dist/src/findings/completion.js +27 -3
- package/dist/src/findings/interfaces.js +6 -2
- package/dist/src/findings/reads-and-writes.js +53 -9
- package/dist/src/findings/unused-declarations.js +40 -1
- package/dist/src/generated-file.js +24 -0
- package/dist/src/interface-satisfaction.js +2 -68
- package/dist/src/inventory.js +4 -1
- package/dist/src/kinds.js +4 -3
- package/dist/src/query.js +15 -0
- package/dist/src/references.js +199 -10
- package/dist/src/resolve.js +12 -11
- package/dist/src/roots.js +1 -1
- package/dist/src/stores.js +3 -0
- package/dist/src/value-flow.js +74 -0
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +92 -13
- package/src/configuration-files.ts +136 -1
- package/src/conformance-record.ts +3 -10
- package/src/contextual-uses.ts +354 -0
- package/src/convention-rows.ts +8 -0
- package/src/conventions.ts +161 -13
- package/src/dependencies.ts +14 -1
- package/src/destructuring.ts +16 -6
- package/src/entry-points.ts +16 -3
- package/src/exempt-classes.ts +6 -1
- package/src/exempt.ts +48 -17
- package/src/file-facts.ts +2 -0
- package/src/findings/completion.ts +31 -3
- package/src/findings/interfaces.ts +9 -2
- package/src/findings/reads-and-writes.ts +70 -9
- package/src/findings/unused-declarations.ts +51 -1
- package/src/generated-file.ts +27 -0
- package/src/interface-satisfaction.ts +1 -92
- package/src/inventory.ts +4 -1
- package/src/kinds.ts +5 -3
- package/src/query.ts +26 -0
- package/src/references.ts +254 -21
- package/src/resolve.ts +14 -13
- package/src/roots.ts +11 -1
- package/src/stores.ts +9 -0
- package/src/value-flow.ts +104 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The values a program hands to a position the checker types from its context: the
|
|
3
|
+
* initializer of a declaration whose type is written, an assignment's right side, an
|
|
4
|
+
* argument, a returned value, an arrow function's expression body, an array element, an
|
|
5
|
+
* object property, and the operand of `as` or `satisfies`.
|
|
6
|
+
*/
|
|
7
|
+
import { isArrayLiteralExpression, isArrowFunction, isAsExpression, isBinaryExpression, isCallExpression, isConditionalExpression, isIdentifier, isNewExpression, isParameterDeclaration, isParenthesizedExpression, isPropertyAssignment, isPropertyDeclaration, isReturnStatement, isSatisfiesExpression, isShorthandPropertyAssignment, isSpreadElement, isVariableDeclaration, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
8
|
+
/** The operators of an assignment whose right side is stored into the left side's type. */
|
|
9
|
+
const STORES = new Set([
|
|
10
|
+
SyntaxKind.EqualsToken,
|
|
11
|
+
SyntaxKind.QuestionQuestionEqualsToken,
|
|
12
|
+
SyntaxKind.BarBarEqualsToken,
|
|
13
|
+
SyntaxKind.AmpersandAmpersandEqualsToken,
|
|
14
|
+
]);
|
|
15
|
+
/** Whether a declaration writes its type, so its initializer is read in that type's context. */
|
|
16
|
+
function writesType(node) {
|
|
17
|
+
return node.type !== undefined;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The values one written expression hands on: the expression itself, or what it
|
|
21
|
+
* evaluates to through parentheses, a conditional and the operators whose result is
|
|
22
|
+
* one of their operands.
|
|
23
|
+
*/
|
|
24
|
+
export function valuesOf(expression) {
|
|
25
|
+
if (isParenthesizedExpression(expression)) {
|
|
26
|
+
return valuesOf(expression.expression);
|
|
27
|
+
}
|
|
28
|
+
if (isConditionalExpression(expression)) {
|
|
29
|
+
return [...valuesOf(expression.whenTrue), ...valuesOf(expression.whenFalse)];
|
|
30
|
+
}
|
|
31
|
+
if (isBinaryExpression(expression)) {
|
|
32
|
+
const operator = expression.operatorToken.kind;
|
|
33
|
+
if (operator === SyntaxKind.QuestionQuestionToken || operator === SyntaxKind.BarBarToken) {
|
|
34
|
+
return [...valuesOf(expression.left), ...valuesOf(expression.right)];
|
|
35
|
+
}
|
|
36
|
+
if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.CommaToken) {
|
|
37
|
+
return valuesOf(expression.right);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return [expression];
|
|
41
|
+
}
|
|
42
|
+
/** The expressions one node hands to a position typed by its context. */
|
|
43
|
+
export function handedOn(node) {
|
|
44
|
+
if ((isVariableDeclaration(node) || isPropertyDeclaration(node) || isParameterDeclaration(node)) &&
|
|
45
|
+
node.initializer !== undefined &&
|
|
46
|
+
writesType(node)) {
|
|
47
|
+
return [node.initializer];
|
|
48
|
+
}
|
|
49
|
+
if (isBinaryExpression(node) && STORES.has(node.operatorToken.kind)) {
|
|
50
|
+
return [node.right];
|
|
51
|
+
}
|
|
52
|
+
if (isCallExpression(node) || isNewExpression(node)) {
|
|
53
|
+
return (node.arguments ?? []).filter((argument) => !isSpreadElement(argument));
|
|
54
|
+
}
|
|
55
|
+
if (isReturnStatement(node)) {
|
|
56
|
+
return node.expression === undefined ? [] : [node.expression];
|
|
57
|
+
}
|
|
58
|
+
if (isArrowFunction(node)) {
|
|
59
|
+
return node.body.kind === SyntaxKind.Block ? [] : [node.body];
|
|
60
|
+
}
|
|
61
|
+
if (isArrayLiteralExpression(node)) {
|
|
62
|
+
return node.elements.filter((element) => !isSpreadElement(element) && element.kind !== SyntaxKind.OmittedExpression);
|
|
63
|
+
}
|
|
64
|
+
if (isPropertyAssignment(node)) {
|
|
65
|
+
return [node.initializer];
|
|
66
|
+
}
|
|
67
|
+
if (isShorthandPropertyAssignment(node)) {
|
|
68
|
+
return isIdentifier(node.name) ? [node.name] : [];
|
|
69
|
+
}
|
|
70
|
+
if (isAsExpression(node) || isSatisfiesExpression(node)) {
|
|
71
|
+
return [node.expression];
|
|
72
|
+
}
|
|
73
|
+
return [];
|
|
74
|
+
}
|
package/dist/src/version.js
CHANGED
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/deadset-ts",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "Deterministic dead-code analysis for TypeScript, class and type members included",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@eslint/js": "10.0.1",
|
|
36
|
-
"@types/node": "24.19.
|
|
36
|
+
"@types/node": "24.19.1",
|
|
37
37
|
"eslint": "10.11.0",
|
|
38
38
|
"fast-check": "4.10.2",
|
|
39
39
|
"prettier": "3.9.9",
|
package/src/analysis.ts
CHANGED
|
@@ -36,14 +36,17 @@ import {
|
|
|
36
36
|
} from "./discover.ts";
|
|
37
37
|
import {
|
|
38
38
|
conventionFailuresFor,
|
|
39
|
+
generatedFiles,
|
|
39
40
|
readConventions,
|
|
40
41
|
type AppliedConvention,
|
|
41
42
|
type ConventionManifest,
|
|
42
43
|
} from "./conventions.ts";
|
|
43
44
|
import { readEdgeSides } from "./edges.ts";
|
|
44
45
|
import { enumGroup } from "./enum-group.ts";
|
|
46
|
+
import { generatedFile } from "./generated-file.ts";
|
|
47
|
+
import { sourceFilesOf } from "./source-files.ts";
|
|
45
48
|
import {
|
|
46
|
-
|
|
49
|
+
detectExemptions,
|
|
47
50
|
disabledClasses,
|
|
48
51
|
exemptionsOf,
|
|
49
52
|
holdingWhile,
|
|
@@ -185,6 +188,8 @@ interface ProjectRead {
|
|
|
185
188
|
readonly typeErrorSkips: readonly TypeErrorSkip[];
|
|
186
189
|
/** The lines each of those skips withholds every finding on. */
|
|
187
190
|
readonly skipped: readonly SkippedUnit[];
|
|
191
|
+
/** The project's generated files, by path below the target root, each with the row naming it. */
|
|
192
|
+
readonly generated: ReadonlyMap<string, string>;
|
|
188
193
|
}
|
|
189
194
|
|
|
190
195
|
/** One target project the run analyzes, as its stage is handed it. */
|
|
@@ -284,6 +289,7 @@ function readProjects<Answer>(
|
|
|
284
289
|
entryFiles: config.ts.entryFiles,
|
|
285
290
|
conventions,
|
|
286
291
|
testFiles: config.ts.testFiles,
|
|
292
|
+
parse: (fileName: string, text: string) => engine.parseSourceFile(fileName, text),
|
|
287
293
|
publishedAPI: config.targetKind === "library",
|
|
288
294
|
};
|
|
289
295
|
const notBuilt: NotBuilt[] = [...discovered.notBuilt];
|
|
@@ -425,6 +431,7 @@ function readProjects<Answer>(
|
|
|
425
431
|
.ownSourceFiles()
|
|
426
432
|
.filter((file) => referenceOnly(file.fileName))
|
|
427
433
|
.flatMap((file) => relativePath(absoluteRoot, file.fileName) ?? []),
|
|
434
|
+
generated: generatedFiles(conventions, sourceFilesOf(project, targetRoot)),
|
|
428
435
|
};
|
|
429
436
|
};
|
|
430
437
|
return stage(project, { configuration, read });
|
|
@@ -599,6 +606,7 @@ export interface ComponentWarning {
|
|
|
599
606
|
const DETECTORS: Detectors = new Map<TSExemptionClass, Detector>([
|
|
600
607
|
["interface-satisfaction", interfaceSatisfaction],
|
|
601
608
|
["enum-group", enumGroup],
|
|
609
|
+
["generated-file", generatedFile],
|
|
602
610
|
["template-field", templateField],
|
|
603
611
|
["reflective-lookup", reflectiveLookup],
|
|
604
612
|
["decorator", decorator],
|
|
@@ -677,11 +685,31 @@ function readRun<Extra>(
|
|
|
677
685
|
projectRead.rooted.liveUnderReachability,
|
|
678
686
|
resolved.testFilePaths,
|
|
679
687
|
);
|
|
688
|
+
const detected = detectExemptions(
|
|
689
|
+
{
|
|
690
|
+
project,
|
|
691
|
+
held: projectRead.held,
|
|
692
|
+
targetRoot,
|
|
693
|
+
templates,
|
|
694
|
+
ts: config.ts,
|
|
695
|
+
consumers,
|
|
696
|
+
...(config.analysis.generatedFiles === "include"
|
|
697
|
+
? {}
|
|
698
|
+
: { generated: projectRead.generated }),
|
|
699
|
+
},
|
|
700
|
+
detectors,
|
|
701
|
+
{
|
|
702
|
+
disabled,
|
|
703
|
+
mode,
|
|
704
|
+
testFiles: new Set([...resolved.testFilePaths, ...support]),
|
|
705
|
+
},
|
|
706
|
+
);
|
|
707
|
+
const exempt = detected.records;
|
|
680
708
|
const made = supportReferences(resolved.references, support);
|
|
681
709
|
const configured: Configured = {
|
|
682
710
|
configuration: projectRead.configuration,
|
|
683
711
|
symbols: projectRead.held.symbols,
|
|
684
|
-
references: made,
|
|
712
|
+
references: [...made, ...evidenceReferences(projectRead.held.symbols, detected.inTestFiles)],
|
|
685
713
|
roots: projectRead.rooted.liveUnderReachability,
|
|
686
714
|
testFiles: resolved.testFilePaths,
|
|
687
715
|
...(support.size === 0 ? {} : { supportFiles: [...support].sort() }),
|
|
@@ -689,15 +717,6 @@ function readRun<Extra>(
|
|
|
689
717
|
? {}
|
|
690
718
|
: { referenceOnly: projectRead.referenceOnly }),
|
|
691
719
|
};
|
|
692
|
-
const exempt = computeExemptions(
|
|
693
|
-
{ project, held: projectRead.held, targetRoot, templates, ts: config.ts, consumers },
|
|
694
|
-
detectors,
|
|
695
|
-
{
|
|
696
|
-
disabled,
|
|
697
|
-
mode,
|
|
698
|
-
testFiles: new Set([...resolved.testFilePaths, ...support]),
|
|
699
|
-
},
|
|
700
|
-
);
|
|
701
720
|
const unanswered = [
|
|
702
721
|
...projectRead.held.unanswered,
|
|
703
722
|
...projectRead.rooted.unanswered,
|
|
@@ -784,6 +803,61 @@ function holdingSweep<Extra>(
|
|
|
784
803
|
}
|
|
785
804
|
}
|
|
786
805
|
|
|
806
|
+
/**
|
|
807
|
+
* The test references exemption evidence written in a test file makes: each is a read of
|
|
808
|
+
* the declaration it would retain, made by the test file at the evidence's site.
|
|
809
|
+
*/
|
|
810
|
+
function evidenceReferences(
|
|
811
|
+
symbols: readonly InventorySymbol[],
|
|
812
|
+
inTestFiles: readonly Exemption[],
|
|
813
|
+
): readonly Reference[] {
|
|
814
|
+
if (inTestFiles.length === 0) {
|
|
815
|
+
return [];
|
|
816
|
+
}
|
|
817
|
+
const files = new Map(
|
|
818
|
+
symbols
|
|
819
|
+
.filter((symbol) => symbol.kind === "file")
|
|
820
|
+
.map((symbol) => [symbol.position.path, symbol.id]),
|
|
821
|
+
);
|
|
822
|
+
return inTestFiles.flatMap((record) => {
|
|
823
|
+
const from = files.get(record.site.path);
|
|
824
|
+
return from === undefined
|
|
825
|
+
? []
|
|
826
|
+
: [
|
|
827
|
+
{
|
|
828
|
+
from,
|
|
829
|
+
to: record.id,
|
|
830
|
+
position: record.site,
|
|
831
|
+
use: "read",
|
|
832
|
+
resolution: "syntax",
|
|
833
|
+
test: true,
|
|
834
|
+
},
|
|
835
|
+
];
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* The declarations every reference the mode counts stores into, a loaded consumer's
|
|
841
|
+
* reference counting as a read: what the write-only kind would report but for a record.
|
|
842
|
+
*/
|
|
843
|
+
function writtenOnly<Extra>(read: ReadRun<Extra>, mode: Mode): ReadonlySet<string> {
|
|
844
|
+
const written = new Set<string>();
|
|
845
|
+
const readSome = new Set<string>();
|
|
846
|
+
for (const { configured } of read.projects) {
|
|
847
|
+
for (const reference of configured.references) {
|
|
848
|
+
if (mode.production && reference.test) {
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
if (reference.use === "write" && reference.consumer === undefined) {
|
|
852
|
+
written.add(reference.to);
|
|
853
|
+
} else {
|
|
854
|
+
readSome.add(reference.to);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return new Set([...written].filter((id) => !readSome.has(id)));
|
|
859
|
+
}
|
|
860
|
+
|
|
787
861
|
/**
|
|
788
862
|
* The sweep of a run read once, under one set of marks. Where a question went unanswered,
|
|
789
863
|
* the run is swept once more without what that question could have kept live and without
|
|
@@ -834,7 +908,7 @@ function sweptOf<Extra>(
|
|
|
834
908
|
matrix: read.matrix,
|
|
835
909
|
sweep: result,
|
|
836
910
|
exempt,
|
|
837
|
-
retained: retainedIn(read.matrix, swept),
|
|
911
|
+
retained: retainedIn(read.matrix, swept, writtenOnly(read, input.mode)),
|
|
838
912
|
notBuilt: read.notBuilt,
|
|
839
913
|
unanswered: read.unanswered,
|
|
840
914
|
heldByUnanswered: [...held],
|
|
@@ -874,6 +948,7 @@ interface EmitterExtra {
|
|
|
874
948
|
readonly needs: ReturnType<typeof projectNeeds>;
|
|
875
949
|
readonly implementations: ReturnType<typeof implementations>;
|
|
876
950
|
readonly heldByInclusion: readonly string[];
|
|
951
|
+
readonly generated: readonly string[];
|
|
877
952
|
readonly rooted: Roots;
|
|
878
953
|
readonly directives: readonly InlineDirective[];
|
|
879
954
|
readonly declarationsNamed: readonly boolean[];
|
|
@@ -900,9 +975,12 @@ function readEmitterRun(
|
|
|
900
975
|
(project, read, references) => ({
|
|
901
976
|
deprecation: deprecatedDeclarations(project, read.held),
|
|
902
977
|
accessors: accessorsOf(project, read.held),
|
|
903
|
-
needs: projectNeeds(project, read.held, host, targetRoot)
|
|
978
|
+
needs: projectNeeds(project, read.held, host, targetRoot, (fileName, text) =>
|
|
979
|
+
engine.parseSourceFile(fileName, text),
|
|
980
|
+
),
|
|
904
981
|
implementations: implementations(project, read.held),
|
|
905
982
|
heldByInclusion: heldByInclusion(project, targetRoot),
|
|
983
|
+
generated: [...read.generated.keys()],
|
|
906
984
|
rooted: read.rooted,
|
|
907
985
|
directives: inlineDirectives(project, read.held, targetRoot).filter(
|
|
908
986
|
(directive) => !read.referenceOnly.includes(directive.site.path),
|
|
@@ -956,6 +1034,7 @@ function emitterInputOver(
|
|
|
956
1034
|
files: {
|
|
957
1035
|
tree: sourceTree(host, targetRoot),
|
|
958
1036
|
heldByInclusion: new Set(projects.flatMap((one) => one.extra.heldByInclusion)),
|
|
1037
|
+
generated: new Set(projects.flatMap((one) => one.extra.generated)),
|
|
959
1038
|
},
|
|
960
1039
|
boundary: {
|
|
961
1040
|
consumers: {
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
|
+
isExportDeclaration,
|
|
12
|
+
isImportDeclaration,
|
|
11
13
|
isNoSubstitutionTemplateLiteral,
|
|
12
14
|
isStringLiteral,
|
|
13
15
|
type Node,
|
|
@@ -43,6 +45,17 @@ const DEPENDENCY_MEMBERS = [
|
|
|
43
45
|
/** The prefixes a string names a file with, relative to the file that writes it. */
|
|
44
46
|
const RELATIVE_PREFIXES = ["./", "../"] as const;
|
|
45
47
|
|
|
48
|
+
/** The JavaScript extensions a relative specifier from a configuration file stands for TypeScript under. */
|
|
49
|
+
const COUNTERPARTS: readonly (readonly [string, readonly string[]])[] = [
|
|
50
|
+
[".js", [".ts", ".tsx"]],
|
|
51
|
+
[".jsx", [".tsx", ".ts"]],
|
|
52
|
+
[".mjs", [".mts"]],
|
|
53
|
+
[".cjs", [".cts"]],
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/** The extensions appended to a relative specifier from a configuration file, in order. */
|
|
57
|
+
const APPENDED = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"] as const;
|
|
58
|
+
|
|
46
59
|
/** The extensions the program's module resolution appends to a specifier written without one. */
|
|
47
60
|
const RESOLVED_EXTENSIONS = [
|
|
48
61
|
".ts",
|
|
@@ -83,10 +96,27 @@ interface ConfigurationDocument {
|
|
|
83
96
|
readonly strings: readonly string[];
|
|
84
97
|
}
|
|
85
98
|
|
|
99
|
+
/** Parses one file the program does not hold, with no type information. */
|
|
100
|
+
export type ParseFile = (fileName: string, text: string) => SourceFile;
|
|
101
|
+
|
|
102
|
+
/** One file no program holds that a configuration module outside the program leads to. */
|
|
103
|
+
export interface OutsideModule {
|
|
104
|
+
/** The file, parsed alone. */
|
|
105
|
+
readonly file: SourceFile;
|
|
106
|
+
/** The directory of the configuration module the file was reached from, which holds its manifest. */
|
|
107
|
+
readonly manifestDir: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
86
110
|
/** The configuration files one project holds and sits beside. */
|
|
87
111
|
interface ConfigurationFiles {
|
|
88
112
|
/** Every own file that is a configuration module, with the form its name has. */
|
|
89
113
|
readonly modules: readonly { readonly file: SourceFile; readonly form: string }[];
|
|
114
|
+
/**
|
|
115
|
+
* Every configuration module in a directory of the project that the program does not
|
|
116
|
+
* hold, and every file such a file imports or re-exports by a relative specifier that
|
|
117
|
+
* the program does not hold either, read where a parser is given.
|
|
118
|
+
*/
|
|
119
|
+
readonly outside: readonly OutsideModule[];
|
|
90
120
|
/** Every JSON configuration file in a directory of the project's files or above one. */
|
|
91
121
|
readonly documents: readonly ConfigurationDocument[];
|
|
92
122
|
}
|
|
@@ -128,6 +158,7 @@ export function configurationFiles(
|
|
|
128
158
|
host: Host,
|
|
129
159
|
targetRoot: string,
|
|
130
160
|
files: SourceFiles,
|
|
161
|
+
parse?: ParseFile,
|
|
131
162
|
): ConfigurationFiles {
|
|
132
163
|
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
133
164
|
const manifests = new Map<string, boolean>();
|
|
@@ -182,7 +213,111 @@ export function configurationFiles(
|
|
|
182
213
|
documents.push({ path, strings: documentStrings(value) });
|
|
183
214
|
}
|
|
184
215
|
}
|
|
185
|
-
return {
|
|
216
|
+
return {
|
|
217
|
+
modules,
|
|
218
|
+
documents,
|
|
219
|
+
outside:
|
|
220
|
+
parse === undefined ? [] : outsideModules(host, [...dirs], files, holdsManifest, parse),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** The relative specifiers one file's imports and re-exports name. */
|
|
225
|
+
function relativeSpecifiers(file: SourceFile): readonly string[] {
|
|
226
|
+
return file.statements.flatMap((statement) => {
|
|
227
|
+
const specifier =
|
|
228
|
+
isImportDeclaration(statement) || isExportDeclaration(statement)
|
|
229
|
+
? statement.moduleSpecifier
|
|
230
|
+
: undefined;
|
|
231
|
+
return specifier !== undefined &&
|
|
232
|
+
isStringLiteral(specifier) &&
|
|
233
|
+
RELATIVE_PREFIXES.some((prefix) => specifier.text.startsWith(prefix))
|
|
234
|
+
? [specifier.text]
|
|
235
|
+
: [];
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The module one relative specifier from a configuration file reaches: the path it names,
|
|
241
|
+
* that path with its JavaScript extension replaced by the TypeScript counterparts, that
|
|
242
|
+
* path with an extension appended, and a directory's `index` with one appended.
|
|
243
|
+
*/
|
|
244
|
+
function reachedBy(host: Host, dir: string, specifier: string): string | undefined {
|
|
245
|
+
const path = resolvePath(dir, specifier);
|
|
246
|
+
const isFile = (candidate: string): boolean => host.kindOf(candidate) === "file";
|
|
247
|
+
// A data file such as `./package.json` is no module, so its strings use no dependency.
|
|
248
|
+
if (isFile(path) && APPENDED.some((extension) => path.endsWith(extension))) {
|
|
249
|
+
return path;
|
|
250
|
+
}
|
|
251
|
+
for (const [extension, counterparts] of COUNTERPARTS) {
|
|
252
|
+
if (path.endsWith(extension)) {
|
|
253
|
+
const stem = path.slice(0, -extension.length);
|
|
254
|
+
const found = counterparts.map((one) => stem + one).find(isFile);
|
|
255
|
+
if (found !== undefined) {
|
|
256
|
+
return found;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const appended = APPENDED.map((extension) => path + extension).find(isFile);
|
|
261
|
+
if (appended !== undefined) {
|
|
262
|
+
return appended;
|
|
263
|
+
}
|
|
264
|
+
return host.kindOf(path) === "directory"
|
|
265
|
+
? APPENDED.map((extension) => joinPath(path, `index${extension}`)).find(isFile)
|
|
266
|
+
: undefined;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* The configuration modules of the directories that hold a manifest which the program
|
|
271
|
+
* does not hold, and the files they import by a relative specifier, until no further
|
|
272
|
+
* file joins. A file that cannot be read is skipped.
|
|
273
|
+
*/
|
|
274
|
+
function outsideModules(
|
|
275
|
+
host: Host,
|
|
276
|
+
dirs: readonly string[],
|
|
277
|
+
files: SourceFiles,
|
|
278
|
+
holdsManifest: (dir: string) => boolean,
|
|
279
|
+
parse: ParseFile,
|
|
280
|
+
): readonly OutsideModule[] {
|
|
281
|
+
const pending: { path: string; manifestDir: string }[] = [];
|
|
282
|
+
for (const dir of [...dirs].sort()) {
|
|
283
|
+
if (!holdsManifest(dir)) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
let names: readonly string[];
|
|
287
|
+
try {
|
|
288
|
+
names = host
|
|
289
|
+
.readDirectory(dir)
|
|
290
|
+
.filter((entry) => !entry.directory && configurationModuleForm(entry.name) !== undefined)
|
|
291
|
+
.map((entry) => entry.name)
|
|
292
|
+
.sort();
|
|
293
|
+
} catch {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
pending.push(...names.map((name) => ({ path: joinPath(dir, name), manifestDir: dir })));
|
|
297
|
+
}
|
|
298
|
+
const found: OutsideModule[] = [];
|
|
299
|
+
const seen = new Set<string>();
|
|
300
|
+
for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
|
|
301
|
+
if (seen.has(next.path) || files.byName.has(next.path)) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
seen.add(next.path);
|
|
305
|
+
let text: string;
|
|
306
|
+
try {
|
|
307
|
+
text = host.readFile(next.path);
|
|
308
|
+
} catch {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const file = parse(next.path, text);
|
|
312
|
+
found.push({ file, manifestDir: next.manifestDir });
|
|
313
|
+
for (const specifier of relativeSpecifiers(file)) {
|
|
314
|
+
const reached = reachedBy(host, dirnamePath(next.path), specifier);
|
|
315
|
+
if (reached !== undefined) {
|
|
316
|
+
pending.push({ path: reached, manifestDir: next.manifestDir });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return found;
|
|
186
321
|
}
|
|
187
322
|
|
|
188
323
|
/** Every dependency the manifest in one directory declares, or none where it holds none to read. */
|
|
@@ -2,16 +2,9 @@
|
|
|
2
2
|
// Regenerate it with `UPDATE_GOLDEN=1 npx vitest --run src/conformance.corpus.test.ts`.
|
|
3
3
|
export const RECORD = {
|
|
4
4
|
conformance: {
|
|
5
|
-
corpusVersion: "2.
|
|
5
|
+
corpusVersion: "2.2.0",
|
|
6
6
|
result: "pass",
|
|
7
|
-
digest: "sha256:
|
|
7
|
+
digest: "sha256:ce7c9087ed7b2baf3de9fa2df436b500706f4f9e94fd9e0fed633054305f06a4",
|
|
8
8
|
},
|
|
9
|
-
gaps: [
|
|
10
|
-
{
|
|
11
|
-
fixture: "private-member-unread",
|
|
12
|
-
capability: "reflective-lookup",
|
|
13
|
-
reason:
|
|
14
|
-
"The reference pass resolves a string-literal element access to the member it names, so the member is live by that reference and the retained listing names no class for it.",
|
|
15
|
-
},
|
|
16
|
-
],
|
|
9
|
+
gaps: [],
|
|
17
10
|
} as const;
|