@cplieger/deadset-ts 4.0.1 → 5.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 +4 -4
- package/dist/bin/component-mapper.js +5 -0
- package/dist/bin/mapper-connection.js +101 -0
- package/dist/bin/node-host.js +24 -2
- package/dist/src/analysis.js +229 -36
- package/dist/src/component-files.js +612 -0
- package/dist/src/component-layer.js +132 -0
- package/dist/src/config.js +9 -2
- package/dist/src/configuration-files.js +186 -0
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/convention-rows.js +383 -0
- package/dist/src/conventions.js +456 -0
- package/dist/src/dependencies.js +30 -3
- package/dist/src/discover.js +10 -3
- package/dist/src/emit-map.js +72 -0
- package/dist/src/entry-point-gaps.js +81 -35
- package/dist/src/entry-points.js +47 -32
- package/dist/src/exempt.js +11 -3
- package/dist/src/findings/completion.js +19 -29
- package/dist/src/findings/emitters.js +22 -1
- package/dist/src/findings/unused-declarations.js +7 -2
- package/dist/src/graph.js +13 -5
- package/dist/src/inventory.js +4 -2
- package/dist/src/kinds.js +2 -2
- package/dist/src/manifest.js +37 -11
- package/dist/src/matrix.js +21 -13
- package/dist/src/partial.js +3 -1
- package/dist/src/pnpm-workspace.js +120 -0
- package/dist/src/position.js +17 -2
- package/dist/src/print.js +2 -0
- package/dist/src/references.js +209 -30
- package/dist/src/report.js +26 -6
- package/dist/src/reporters/annotations.js +10 -3
- package/dist/src/reporters/template.js +3 -0
- package/dist/src/resolve.js +31 -6
- package/dist/src/roots.js +37 -8
- package/dist/src/run.js +13 -3
- package/dist/src/schema.js +2 -0
- package/dist/src/session.js +141 -34
- package/dist/src/setup-failure.js +23 -0
- package/dist/src/source-files.js +4 -61
- package/dist/src/sweep.js +4 -2
- package/dist/src/template-field.js +38 -6
- package/dist/src/test-support.js +59 -0
- package/dist/src/type-errors.js +252 -0
- package/dist/src/type-query-alias.js +92 -0
- package/dist/src/verbs/analyze.js +24 -4
- package/dist/src/verbs/explain.js +4 -1
- package/dist/src/verbs/print-projects.js +17 -56
- package/dist/src/verbs/print-retained.js +7 -2
- package/dist/src/verbs/print-roots.js +7 -2
- package/dist/src/version-range.js +255 -0
- package/dist/src/version.js +1 -1
- package/dist/src/workspace-manifests.js +68 -0
- package/dist/src/workspace-programs.js +342 -0
- package/dist/src/workspace-resolution.js +307 -0
- package/dist/src/workspace-run.js +75 -0
- package/dist/src/workspace.js +358 -0
- package/jsr.json +1 -1
- package/package.json +2 -1
- package/src/analysis.ts +394 -70
- package/src/component-files.ts +718 -0
- package/src/component-layer.ts +160 -0
- package/src/config.ts +11 -2
- package/src/configuration-files.ts +235 -0
- package/src/conformance-record.ts +2 -2
- package/src/convention-rows.ts +440 -0
- package/src/conventions.ts +640 -0
- package/src/dependencies.ts +36 -2
- package/src/discover.ts +10 -6
- package/src/emit-map.ts +93 -0
- package/src/entry-point-gaps.ts +92 -38
- package/src/entry-points.ts +66 -36
- package/src/exempt.ts +21 -3
- package/src/findings/completion.ts +24 -33
- package/src/findings/emitters.ts +32 -1
- package/src/findings/unused-declarations.ts +9 -2
- package/src/graph.ts +15 -4
- package/src/host.ts +23 -0
- package/src/index.ts +1 -1
- package/src/inventory.ts +13 -2
- package/src/kinds.ts +2 -2
- package/src/manifest.ts +47 -14
- package/src/matrix.ts +43 -15
- package/src/partial.ts +6 -1
- package/src/pnpm-workspace.ts +135 -0
- package/src/position.ts +23 -2
- package/src/print.ts +2 -0
- package/src/references.ts +227 -28
- package/src/report.ts +56 -6
- package/src/reporters/annotations.ts +12 -3
- package/src/reporters/template.ts +3 -0
- package/src/resolve.ts +59 -6
- package/src/roots.ts +60 -9
- package/src/run.ts +15 -5
- package/src/schema.ts +2 -0
- package/src/session.ts +218 -37
- package/src/setup-failure.ts +39 -0
- package/src/source-files.ts +4 -66
- package/src/sweep.ts +9 -2
- package/src/template-field.ts +42 -7
- package/src/test-support.ts +77 -0
- package/src/type-errors.ts +340 -0
- package/src/type-query-alias.ts +133 -0
- package/src/verbs/analyze.ts +38 -4
- package/src/verbs/explain.ts +4 -1
- package/src/verbs/print-projects.ts +17 -69
- package/src/verbs/print-retained.ts +7 -2
- package/src/verbs/print-roots.ts +7 -2
- package/src/verbs/verb.ts +6 -3
- package/src/version-range.ts +309 -0
- package/src/version.ts +1 -1
- package/src/workspace-manifests.ts +100 -0
- package/src/workspace-programs.ts +486 -0
- package/src/workspace-resolution.ts +393 -0
- package/src/workspace-run.ts +148 -0
- package/src/workspace.ts +463 -0
package/dist/src/session.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { API, SymbolFlags } from "@typescript/native/unstable/sync";
|
|
2
|
+
import { componentLayer, mapperManifest } from "./component-layer.js";
|
|
2
3
|
import { DiscoveryError } from "./discover.js";
|
|
4
|
+
import { joinPath } from "./paths.js";
|
|
3
5
|
import { queriesOf, UnansweredLog, } from "./query.js";
|
|
4
6
|
/**
|
|
5
7
|
* Opens the compiler client the analysis reads through.
|
|
@@ -9,11 +11,55 @@ import { queriesOf, UnansweredLog, } from "./query.js";
|
|
|
9
11
|
* second call has nothing left to do.
|
|
10
12
|
*/
|
|
11
13
|
export function openEngine(options) {
|
|
12
|
-
const
|
|
14
|
+
const { components } = options;
|
|
15
|
+
let mapper;
|
|
16
|
+
if (components !== undefined && components.extensions.length > 0) {
|
|
17
|
+
const made = components.host.temporaryDirectory();
|
|
18
|
+
try {
|
|
19
|
+
components.host.writeDocument(joinPath(made.path, "package.json"), [
|
|
20
|
+
mapperManifest(components.host.componentMapperCommand()),
|
|
21
|
+
]);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
made.remove();
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
mapper = made;
|
|
28
|
+
}
|
|
29
|
+
const api = new API({
|
|
30
|
+
collectTiming: options.collectTiming,
|
|
31
|
+
...(mapper === undefined ? {} : { runExternalCode: true }),
|
|
32
|
+
});
|
|
13
33
|
let closed = false;
|
|
14
34
|
return {
|
|
15
35
|
parseConfigFile: (file) => api.parseConfigFile(file),
|
|
16
|
-
|
|
36
|
+
parseConfigJson: (json, configFile) => api.parseJsonConfigFileContent(json, { configFileName: configFile }),
|
|
37
|
+
createSnapshot: (openProjects, createPrograms, configurations = new Map()) => api.createSnapshot({
|
|
38
|
+
openProjects: [...openProjects],
|
|
39
|
+
...(createPrograms === undefined ? {} : { createPrograms: [...createPrograms] }),
|
|
40
|
+
...(components === undefined
|
|
41
|
+
? configurations.size === 0
|
|
42
|
+
? {}
|
|
43
|
+
: { fileSystem: { kind: "layer", files: Object.fromEntries(configurations) } }
|
|
44
|
+
: {
|
|
45
|
+
fileSystem: componentLayer(components.host, openProjects, mapper === undefined
|
|
46
|
+
? undefined
|
|
47
|
+
: { extensions: components.extensions, packageDirectory: mapper.path }, configurations),
|
|
48
|
+
}),
|
|
49
|
+
}),
|
|
50
|
+
readsComponents: mapper !== undefined,
|
|
51
|
+
createModuleResolver: (options, entries) => api.createModuleResolver(options, {
|
|
52
|
+
moduleResolutions: { fallback: "resolve", entries: [...entries] },
|
|
53
|
+
}),
|
|
54
|
+
parseSourceFile: (fileName, text) => {
|
|
55
|
+
const retained = api.createSourceFile(fileName, text);
|
|
56
|
+
try {
|
|
57
|
+
return retained.sourceFile;
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
retained.dispose();
|
|
61
|
+
}
|
|
62
|
+
},
|
|
17
63
|
batch: (questions) => api.batch(...questions),
|
|
18
64
|
ask: (_accessor, _locations, question) => question(),
|
|
19
65
|
getTimingInfo: () => api.getTimingInfo(),
|
|
@@ -22,10 +68,35 @@ export function openEngine(options) {
|
|
|
22
68
|
return;
|
|
23
69
|
}
|
|
24
70
|
closed = true;
|
|
25
|
-
|
|
71
|
+
try {
|
|
72
|
+
api.close();
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
mapper?.remove();
|
|
76
|
+
}
|
|
26
77
|
},
|
|
27
78
|
};
|
|
28
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* The source files of one program that `own` admits, in the program's order, none of
|
|
82
|
+
* them one of the compiler's default libraries.
|
|
83
|
+
*/
|
|
84
|
+
function filesOwned(program, own) {
|
|
85
|
+
return (program
|
|
86
|
+
.getSourceFileNames()
|
|
87
|
+
// The metadata decides which files belong to the target, and it is read by
|
|
88
|
+
// name. Fetching each file first and filtering afterwards costs one round
|
|
89
|
+
// trip per library file the answer then discards, and a program holds far
|
|
90
|
+
// more library files than target files.
|
|
91
|
+
.filter((name) => {
|
|
92
|
+
const metadata = program.getSourceFileMetadata(name);
|
|
93
|
+
return (metadata !== undefined &&
|
|
94
|
+
!metadata.isDefaultLibrary &&
|
|
95
|
+
own(name, metadata.isFromExternalLibrary));
|
|
96
|
+
})
|
|
97
|
+
.map((name) => program.getSourceFile(name))
|
|
98
|
+
.filter((file) => file !== undefined));
|
|
99
|
+
}
|
|
29
100
|
function viewOf(project, configFile, engine, log) {
|
|
30
101
|
const queries = queriesOf(project.checker, configFile, (questions) => engine.batch(questions), log, (accessor, locations, question) => engine.ask(accessor, locations, question));
|
|
31
102
|
// The snapshot is never updated, so a project's own file set is the same answer
|
|
@@ -35,23 +106,13 @@ function viewOf(project, configFile, engine, log) {
|
|
|
35
106
|
let ownFiles;
|
|
36
107
|
let ownKeys;
|
|
37
108
|
const ownSourceFiles = () => {
|
|
38
|
-
ownFiles ??= project.program
|
|
39
|
-
.getSourceFileNames()
|
|
40
|
-
// The metadata decides which files belong to the target, and it is read by
|
|
41
|
-
// name. Fetching each file first and filtering afterwards costs one round
|
|
42
|
-
// trip per library file the answer then discards, and a program holds far
|
|
43
|
-
// more library files than target files.
|
|
44
|
-
.filter((name) => {
|
|
45
|
-
const metadata = project.program.getSourceFileMetadata(name);
|
|
46
|
-
return (metadata !== undefined && !metadata.isDefaultLibrary && !metadata.isFromExternalLibrary);
|
|
47
|
-
})
|
|
48
|
-
.map((name) => project.program.getSourceFile(name))
|
|
49
|
-
.filter((file) => file !== undefined);
|
|
109
|
+
ownFiles ??= filesOwned(project.program, (_name, fromExternalLibrary) => !fromExternalLibrary);
|
|
50
110
|
return ownFiles;
|
|
51
111
|
};
|
|
52
112
|
return {
|
|
53
113
|
configFile,
|
|
54
114
|
program: project.program,
|
|
115
|
+
rootFiles: project.parsedCommandLine.fileNames,
|
|
55
116
|
queries,
|
|
56
117
|
ownSourceFiles,
|
|
57
118
|
ownPaths: () => {
|
|
@@ -101,6 +162,26 @@ export function narrowedTo(project, keep) {
|
|
|
101
162
|
},
|
|
102
163
|
};
|
|
103
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* The same project, its own source files the ones `own` admits among every file its
|
|
167
|
+
* program holds, given whether the compiler reached the file as a library's.
|
|
168
|
+
*/
|
|
169
|
+
export function ownedAs(project, own) {
|
|
170
|
+
let owned;
|
|
171
|
+
let keys;
|
|
172
|
+
const ownSourceFiles = () => {
|
|
173
|
+
owned ??= filesOwned(project.program, own);
|
|
174
|
+
return owned;
|
|
175
|
+
};
|
|
176
|
+
return {
|
|
177
|
+
...project,
|
|
178
|
+
ownSourceFiles,
|
|
179
|
+
ownPaths: () => {
|
|
180
|
+
keys ??= new Set(ownSourceFiles().map((file) => file.path));
|
|
181
|
+
return keys;
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
104
185
|
/** The three diagnostic sets one project carries, read before any analysis. */
|
|
105
186
|
export function diagnosticsOf(project) {
|
|
106
187
|
return {
|
|
@@ -119,28 +200,54 @@ export function diagnosticsOf(project) {
|
|
|
119
200
|
};
|
|
120
201
|
}
|
|
121
202
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* The snapshot is opened once with every configuration, so each becomes a project
|
|
126
|
-
* with its own program and checker; it is never updated, because the analysis
|
|
127
|
-
* reads and the tree does not change under it. The order the snapshot lists its
|
|
128
|
-
* projects in is the compiler's own, so each named configuration is looked up in it
|
|
129
|
-
* instead. A configuration the snapshot opened no project for ends the run under
|
|
130
|
-
* `"refuse"` and is named in the result under `"record"`. The snapshot is disposed once
|
|
131
|
-
* the last project has been visited, and the client is released after it, whether the
|
|
132
|
-
* visit completed or threw.
|
|
203
|
+
* The projects one snapshot holds for the configurations named, by configuration file. A
|
|
204
|
+
* project opened from a written configuration is the project of the configuration
|
|
205
|
+
* `written` maps it to.
|
|
133
206
|
*/
|
|
134
|
-
|
|
207
|
+
function projectsOf(snapshot, built, written) {
|
|
208
|
+
const opened = new Map();
|
|
209
|
+
for (const project of snapshot.getProjects()) {
|
|
210
|
+
if (project.configFileName !== undefined) {
|
|
211
|
+
opened.set(written.get(project.configFileName) ?? project.configFileName, project);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
(snapshot.operation.createdPrograms ?? []).forEach((program, at) => {
|
|
215
|
+
const project = snapshot.getProject(program.id);
|
|
216
|
+
const configFile = built[at];
|
|
217
|
+
if (project !== undefined && configFile !== undefined) {
|
|
218
|
+
opened.set(configFile, project);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
return opened;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Runs `visit` over every project the compiler configurations name, inside one never updated
|
|
225
|
+
* snapshot of one client, and answers in their order. A configuration `programs` describes is
|
|
226
|
+
* built as that program, or opened from the configuration it writes, instead of as a project.
|
|
227
|
+
* One the snapshot holds nothing for ends the run under `"refuse"` and is named in the result
|
|
228
|
+
* under `"record"`. The snapshot is disposed after the last visit and the client released
|
|
229
|
+
* after it, whether the visit completed or threw.
|
|
230
|
+
*/
|
|
231
|
+
export function runSession(engine, configFiles, visit, unopened = "refuse", programs = new Map()) {
|
|
135
232
|
try {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
233
|
+
const built = [];
|
|
234
|
+
const params = [];
|
|
235
|
+
const written = new Map();
|
|
236
|
+
const texts = new Map();
|
|
237
|
+
for (const configFile of configFiles) {
|
|
238
|
+
const program = programs.get(configFile);
|
|
239
|
+
if (program?.kind === "program") {
|
|
240
|
+
built.push(configFile);
|
|
241
|
+
params.push(program.params);
|
|
242
|
+
}
|
|
243
|
+
else if (program?.kind === "configuration") {
|
|
244
|
+
written.set(program.file, configFile);
|
|
245
|
+
texts.set(program.file, program.text);
|
|
143
246
|
}
|
|
247
|
+
}
|
|
248
|
+
const snapshot = engine.createSnapshot([...configFiles.filter((configFile) => !programs.has(configFile)), ...written.keys()], params.length === 0 ? undefined : params, texts);
|
|
249
|
+
try {
|
|
250
|
+
const opened = projectsOf(snapshot, built, written);
|
|
144
251
|
const log = new UnansweredLog();
|
|
145
252
|
const projects = [];
|
|
146
253
|
const missing = [];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A setup failure: a file or a component the analysis needs and the project does not
|
|
3
|
+
* provide. Each one names what is missing and the fix, on one line of the error stream
|
|
4
|
+
* that starts with its class.
|
|
5
|
+
*/
|
|
6
|
+
/** The failure as the one line of the error stream that names it. */
|
|
7
|
+
export function setupLine(failure) {
|
|
8
|
+
return `setup failure: ${failure.setupClass}: ${failure.detail}`;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A run that met a setup failure where none may be dropped: in a configuration the
|
|
12
|
+
* configuration document or the scope declares, in a declared consumer, or in every
|
|
13
|
+
* configuration discovery derived. It ends the run with the failure code and no finding
|
|
14
|
+
* list.
|
|
15
|
+
*/
|
|
16
|
+
export class SetupError extends Error {
|
|
17
|
+
failures;
|
|
18
|
+
constructor(failures) {
|
|
19
|
+
super(failures.map(setupLine).join("\n"));
|
|
20
|
+
this.name = "SetupError";
|
|
21
|
+
this.failures = failures;
|
|
22
|
+
}
|
|
23
|
+
}
|
package/dist/src/source-files.js
CHANGED
|
@@ -3,45 +3,10 @@
|
|
|
3
3
|
* in a document or a string names one: as the file itself, or as a file the
|
|
4
4
|
* compiler emits from it.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
const EMITTED = [
|
|
9
|
-
{ emitted: ".d.ts", sources: [".ts", ".tsx"] },
|
|
10
|
-
{ emitted: ".d.mts", sources: [".mts"] },
|
|
11
|
-
{ emitted: ".d.cts", sources: [".cts"] },
|
|
12
|
-
{ emitted: ".js", sources: [".ts", ".tsx"] },
|
|
13
|
-
{ emitted: ".jsx", sources: [".tsx"] },
|
|
14
|
-
{ emitted: ".mjs", sources: [".mts"] },
|
|
15
|
-
{ emitted: ".cjs", sources: [".cts"] },
|
|
16
|
-
];
|
|
6
|
+
import { commonDirectory, emittedFrom } from "./emit-map.js";
|
|
7
|
+
import { dirnamePath, relativePath, resolvePath } from "./paths.js";
|
|
17
8
|
/** The one character a subpath pattern of a manifest's `exports` holds. */
|
|
18
9
|
const SUBPATH_WILDCARD = "*";
|
|
19
|
-
/** One path with its extension replaced, where it carries the one named. */
|
|
20
|
-
function reExtended(path, emitted, source) {
|
|
21
|
-
return path.endsWith(emitted) ? path.slice(0, path.length - emitted.length) + source : undefined;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* The longest directory every path shares, which is the directory the compiler
|
|
25
|
-
* emits from where the configuration declares no root directory.
|
|
26
|
-
*/
|
|
27
|
-
function commonDirectory(paths) {
|
|
28
|
-
const first = paths[0];
|
|
29
|
-
if (first === undefined) {
|
|
30
|
-
return "";
|
|
31
|
-
}
|
|
32
|
-
let common = dirnamePath(first).split("/");
|
|
33
|
-
for (const path of paths.slice(1)) {
|
|
34
|
-
const segments = dirnamePath(path).split("/");
|
|
35
|
-
let shared = 0;
|
|
36
|
-
while (shared < common.length &&
|
|
37
|
-
shared < segments.length &&
|
|
38
|
-
common[shared] === segments[shared]) {
|
|
39
|
-
shared += 1;
|
|
40
|
-
}
|
|
41
|
-
common = common.slice(0, shared);
|
|
42
|
-
}
|
|
43
|
-
return common.join("/");
|
|
44
|
-
}
|
|
45
10
|
/**
|
|
46
11
|
* Whether one path is what a subpath pattern stands for: the text before the
|
|
47
12
|
* wildcard, any run of characters including the solidus, then the text after it.
|
|
@@ -77,31 +42,9 @@ export function sourceFilesOf(project, targetRoot) {
|
|
|
77
42
|
const outDir = at(options.outDir);
|
|
78
43
|
const declarationDir = at(options.declarationDir);
|
|
79
44
|
const sourceDir = at(options.rootDir) ?? commonDirectory([...byName.keys()]);
|
|
80
|
-
|
|
81
|
-
const fromEmit = (path, emitted) => {
|
|
82
|
-
const emitDir = emitted.startsWith(".d.") ? (declarationDir ?? outDir) : outDir;
|
|
83
|
-
if (emitDir === undefined) {
|
|
84
|
-
return undefined;
|
|
85
|
-
}
|
|
86
|
-
const below = relativePath(emitDir, path);
|
|
87
|
-
return below === undefined ? undefined : joinPath(sourceDir, below);
|
|
88
|
-
};
|
|
45
|
+
const layout = { outDir, declarationDir, sourceDirs: [sourceDir] };
|
|
89
46
|
/** The source paths one written path may stand for, the path itself first. */
|
|
90
|
-
const candidates = (path) =>
|
|
91
|
-
const found = [path];
|
|
92
|
-
for (const { emitted, sources } of EMITTED) {
|
|
93
|
-
const mapped = fromEmit(path, emitted);
|
|
94
|
-
for (const source of sources) {
|
|
95
|
-
for (const from of [path, mapped]) {
|
|
96
|
-
const candidate = from === undefined ? undefined : reExtended(from, emitted, source);
|
|
97
|
-
if (candidate !== undefined) {
|
|
98
|
-
found.push(candidate);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return found;
|
|
104
|
-
};
|
|
47
|
+
const candidates = (path) => [path, ...emittedFrom(path, layout)];
|
|
105
48
|
return {
|
|
106
49
|
byPath,
|
|
107
50
|
byName,
|
package/dist/src/sweep.js
CHANGED
|
@@ -153,13 +153,15 @@ function reachable(graph, held, mode) {
|
|
|
153
153
|
function admitTestsOfDeadCode(graph, dead, held) {
|
|
154
154
|
const admitted = graph.symbols.map(() => false);
|
|
155
155
|
graph.symbols.forEach((_symbol, at) => {
|
|
156
|
-
|
|
156
|
+
const testCode = graph.test[at] === true || graph.support[at] === true;
|
|
157
|
+
if (!testCode || graph.subject[at] !== true || held[at] === true) {
|
|
157
158
|
return;
|
|
158
159
|
}
|
|
159
160
|
let targets = 0;
|
|
160
161
|
let live = 0;
|
|
161
162
|
for (const edge of graph.out[at] ?? []) {
|
|
162
|
-
|
|
163
|
+
const target = graph.test[edge.to] === true || graph.support[edge.to] === true;
|
|
164
|
+
if (target || graph.subject[edge.to] !== true) {
|
|
163
165
|
continue;
|
|
164
166
|
}
|
|
165
167
|
targets += 1;
|
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
* instance members of classes and the members of interfaces and object types, on any
|
|
12
12
|
* type, because nothing ties the name to a type.
|
|
13
13
|
*/
|
|
14
|
+
import { readComponent } from "./component-files.js";
|
|
14
15
|
import { ConfigError } from "./config.js";
|
|
15
16
|
import { memberNames } from "./member-names.js";
|
|
16
|
-
import { isAbsolutePath, joinPath, normalizePath } from "./paths.js";
|
|
17
|
+
import { isAbsolutePath, joinPath, normalizePath, relativePath } from "./paths.js";
|
|
18
|
+
import { isComponentFile } from "./position.js";
|
|
17
19
|
/** The kinds of declaration an action can name. */
|
|
18
20
|
const READ_BY_A_TEMPLATE = new Set([
|
|
19
21
|
"method",
|
|
@@ -21,6 +23,11 @@ const READ_BY_A_TEMPLATE = new Set([
|
|
|
21
23
|
"interface-method",
|
|
22
24
|
"type-member",
|
|
23
25
|
]);
|
|
26
|
+
/**
|
|
27
|
+
* The kinds of declaration a component file's markup can name. Its expressions are the
|
|
28
|
+
* language's own, so a static member and an enum member are reachable by name too.
|
|
29
|
+
*/
|
|
30
|
+
const READ_BY_MARKUP = new Set([...READ_BY_A_TEMPLATE, "enum-member"]);
|
|
24
31
|
/** One identifier, as the grammar of the analyzed language spells one. */
|
|
25
32
|
const IDENTIFIER = /[\p{ID_Start}$_][\p{ID_Continue}$\u200C\u200D]*/uy;
|
|
26
33
|
/** Two strings ordered bytewise. */
|
|
@@ -133,6 +140,14 @@ function namesOf(text, delimiters) {
|
|
|
133
140
|
from = close + delimiters.right.length;
|
|
134
141
|
}
|
|
135
142
|
}
|
|
143
|
+
/** Every identifier the markup of one component file writes in an action. */
|
|
144
|
+
function markupNamesOf(text) {
|
|
145
|
+
return readComponent(text).markup.flatMap((one) => identifiersOf(one.text).map(({ name, offset }) => ({
|
|
146
|
+
name,
|
|
147
|
+
offset: one.start + offset,
|
|
148
|
+
action: one.action,
|
|
149
|
+
})));
|
|
150
|
+
}
|
|
136
151
|
/** Where one offset of a template is, its column counted in UTF-16 code units. */
|
|
137
152
|
function positionIn(file, offset) {
|
|
138
153
|
const before = file.text.slice(0, offset);
|
|
@@ -146,25 +161,42 @@ function positionIn(file, offset) {
|
|
|
146
161
|
/**
|
|
147
162
|
* The template-field detector: every instance member of a class and every member of
|
|
148
163
|
* an interface or an object type whose name an action of a configured template
|
|
149
|
-
* writes,
|
|
164
|
+
* writes, and every member of those and of an enum whose name a component file's
|
|
165
|
+
* markup writes, recorded at the identifier and held while that file is live.
|
|
150
166
|
*/
|
|
151
167
|
export function templateField(input) {
|
|
152
168
|
const { templates } = input;
|
|
153
|
-
|
|
169
|
+
const components = input.project.ownSourceFiles().flatMap((file) => {
|
|
170
|
+
const path = isComponentFile(file) ? relativePath(input.targetRoot, file.fileName) : undefined;
|
|
171
|
+
return path === undefined ? [] : [{ path, text: file.originalText }];
|
|
172
|
+
});
|
|
173
|
+
if (templates.files.length === 0 && components.length === 0) {
|
|
154
174
|
return [];
|
|
155
175
|
}
|
|
156
176
|
const members = memberNames(input.project.ownSourceFiles(), input.held);
|
|
157
177
|
const found = [];
|
|
158
|
-
|
|
159
|
-
|
|
178
|
+
const scanned = [
|
|
179
|
+
...templates.files.map((file) => ({
|
|
180
|
+
file,
|
|
181
|
+
names: namesOf(file.text, templates.delimiters),
|
|
182
|
+
markup: false,
|
|
183
|
+
})),
|
|
184
|
+
...components.map((file) => ({ file, names: markupNamesOf(file.text), markup: true })),
|
|
185
|
+
];
|
|
186
|
+
for (const { file, names, markup } of scanned) {
|
|
187
|
+
for (const named of names) {
|
|
160
188
|
for (const member of members.get(named.name) ?? []) {
|
|
161
|
-
|
|
189
|
+
const readable = markup
|
|
190
|
+
? READ_BY_MARKUP.has(member.kind)
|
|
191
|
+
: READ_BY_A_TEMPLATE.has(member.kind) && !member.static;
|
|
192
|
+
if (!readable) {
|
|
162
193
|
continue;
|
|
163
194
|
}
|
|
164
195
|
found.push({
|
|
165
196
|
id: member.id,
|
|
166
197
|
detail: `named by ${named.action}`,
|
|
167
198
|
site: positionIn(file, named.offset),
|
|
199
|
+
...(markup ? { whileLive: file.path } : {}),
|
|
168
200
|
});
|
|
169
201
|
}
|
|
170
202
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test-support code: a file that is not a test file by name and that only test code
|
|
3
|
+
* imports. At least one test file reaches it, directly or through other such files, and
|
|
4
|
+
* nothing else does, so it is judged with the tests: what it references is referenced by
|
|
5
|
+
* test code, and a declaration of it is live while test code references it.
|
|
6
|
+
*/
|
|
7
|
+
/** The root kind that never takes a file out of test-support code: the skip only withholds. */
|
|
8
|
+
const SKIP_ROOT = "type-error";
|
|
9
|
+
/**
|
|
10
|
+
* The files of one project that are test-support code, below the target root. A file
|
|
11
|
+
* holding a root is not one, because a root has a caller outside the import graph: a
|
|
12
|
+
* runtime, a tool, a configuration, or a consumer of a library's published API.
|
|
13
|
+
* `testFiles` are the files the test-file rules classified.
|
|
14
|
+
*/
|
|
15
|
+
export function testSupportFiles(symbols, references, roots, testFiles) {
|
|
16
|
+
const fileOf = new Map(symbols.map((symbol) => [symbol.id, symbol.position.path]));
|
|
17
|
+
const users = new Map();
|
|
18
|
+
for (const reference of references) {
|
|
19
|
+
const used = fileOf.get(reference.to);
|
|
20
|
+
const from = reference.position.path;
|
|
21
|
+
if (reference.consumer !== undefined || used === undefined || used === from) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const held = users.get(used);
|
|
25
|
+
if (held === undefined) {
|
|
26
|
+
users.set(used, new Set([from]));
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
held.add(from);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const tests = new Set(testFiles);
|
|
33
|
+
const rooted = new Set(roots.filter((root) => root.kind !== SKIP_ROOT).flatMap((root) => fileOf.get(root.id) ?? []));
|
|
34
|
+
const candidates = [...users.keys()].filter((path) => !tests.has(path) && !rooted.has(path));
|
|
35
|
+
const support = new Set();
|
|
36
|
+
for (let changed = true; changed;) {
|
|
37
|
+
changed = false;
|
|
38
|
+
for (const path of candidates) {
|
|
39
|
+
if (support.has(path)) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const from = users.get(path) ?? new Set();
|
|
43
|
+
if ([...from].every((one) => tests.has(one) || support.has(one))) {
|
|
44
|
+
support.add(path);
|
|
45
|
+
changed = true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return support;
|
|
50
|
+
}
|
|
51
|
+
/** The references with every one a test-support file makes counted as a test reference. */
|
|
52
|
+
export function supportReferences(references, support) {
|
|
53
|
+
if (support.size === 0) {
|
|
54
|
+
return references;
|
|
55
|
+
}
|
|
56
|
+
return references.map((reference) => !reference.test && support.has(reference.position.path)
|
|
57
|
+
? { ...reference, test: true }
|
|
58
|
+
: reference);
|
|
59
|
+
}
|