@cplieger/deadset-ts 4.0.0 → 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 +68 -50
- 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 +230 -37
- 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 +11 -4
- 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 +6 -3
- 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 +4 -2
- 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 +395 -71
- 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 +11 -7
- 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 +6 -3
- package/src/resolve.ts +59 -6
- package/src/roots.ts +60 -9
- package/src/run.ts +15 -5
- package/src/schema.ts +4 -2
- 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/src/session.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { API, SymbolFlags } from "@typescript/native/unstable/sync";
|
|
2
2
|
import type {
|
|
3
3
|
APIRequestGenerator,
|
|
4
|
+
CreateSnapshotProgramParams,
|
|
4
5
|
Diagnostic,
|
|
5
6
|
DocumentIdentifier,
|
|
7
|
+
ModuleResolutionEntry,
|
|
8
|
+
ModuleResolver,
|
|
6
9
|
NodeHandle,
|
|
7
10
|
Project,
|
|
8
11
|
Program,
|
|
@@ -11,7 +14,11 @@ import type {
|
|
|
11
14
|
TimingInfo,
|
|
12
15
|
} from "@typescript/native/unstable/sync";
|
|
13
16
|
import type { Node, SourceFile } from "@typescript/native/unstable/ast";
|
|
17
|
+
import { componentLayer, mapperManifest } from "./component-layer.ts";
|
|
14
18
|
import { DiscoveryError } from "./discover.ts";
|
|
19
|
+
import type { Host, TemporaryDirectory } from "./host.ts";
|
|
20
|
+
import type { WorkspaceProgram } from "./workspace-programs.ts";
|
|
21
|
+
import { joinPath } from "./paths.ts";
|
|
15
22
|
import {
|
|
16
23
|
queriesOf,
|
|
17
24
|
UnansweredLog,
|
|
@@ -36,8 +43,34 @@ export interface Engine {
|
|
|
36
43
|
* the compiler package returns here is not one of its exported names.
|
|
37
44
|
*/
|
|
38
45
|
parseConfigFile(file: DocumentIdentifier): ReturnType<API["parseConfigFile"]>;
|
|
39
|
-
/**
|
|
40
|
-
|
|
46
|
+
/** Resolves one configuration's JSON as if `configFile` held it, which no file need. */
|
|
47
|
+
parseConfigJson(json: unknown, configFile: string): ReturnType<API["parseConfigFile"]>;
|
|
48
|
+
/**
|
|
49
|
+
* Opens one snapshot holding every project named and every program described, the
|
|
50
|
+
* programs in the order given in the snapshot's operation. A project named in
|
|
51
|
+
* `configurations` is opened from that configuration text, which the snapshot alone
|
|
52
|
+
* holds.
|
|
53
|
+
*/
|
|
54
|
+
createSnapshot(
|
|
55
|
+
openProjects: readonly string[],
|
|
56
|
+
createPrograms?: readonly CreateSnapshotProgramParams[],
|
|
57
|
+
configurations?: ReadonlyMap<string, string>,
|
|
58
|
+
): Snapshot;
|
|
59
|
+
/**
|
|
60
|
+
* Whether the snapshots read component files. A described program reads none, because
|
|
61
|
+
* the compiler gives a program no content mapper.
|
|
62
|
+
*/
|
|
63
|
+
readonly readsComponents: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* A module resolver answering each entry's import with the entry's file, and every
|
|
66
|
+
* other import as the compiler resolves it.
|
|
67
|
+
*/
|
|
68
|
+
createModuleResolver(
|
|
69
|
+
options: CreateSnapshotProgramParams["compilerOptions"],
|
|
70
|
+
entries: readonly ModuleResolutionEntry[],
|
|
71
|
+
): ModuleResolver;
|
|
72
|
+
/** Parses one file's text outside every program. */
|
|
73
|
+
parseSourceFile(fileName: string, text: string): SourceFile;
|
|
41
74
|
/** Sends many questions in one round trip and answers each, in order. */
|
|
42
75
|
batch<T>(questions: readonly APIRequestGenerator<T>[]): T[];
|
|
43
76
|
/**
|
|
@@ -56,6 +89,20 @@ export interface Engine {
|
|
|
56
89
|
export interface EngineOptions {
|
|
57
90
|
/** Collect per-request timing, which the calibration record reads. */
|
|
58
91
|
readonly collectTiming: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* The component files every snapshot reads. Present, every configuration a snapshot opens
|
|
94
|
+
* takes this analyzer's mapper in place of its own, or no mapper where no extension is
|
|
95
|
+
* listed, and with an extension the client runs that mapper from a temporary directory the
|
|
96
|
+
* close removes. Absent, the configurations are opened as they are and no mapper runs.
|
|
97
|
+
*/
|
|
98
|
+
readonly components?: ComponentReading;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Which files a run reads as component files, and the host that runs their mapper. */
|
|
102
|
+
export interface ComponentReading {
|
|
103
|
+
/** The file name extensions of component files, `ts.component_extensions`. */
|
|
104
|
+
readonly extensions: readonly string[];
|
|
105
|
+
readonly host: Host;
|
|
59
106
|
}
|
|
60
107
|
|
|
61
108
|
/**
|
|
@@ -66,11 +113,61 @@ export interface EngineOptions {
|
|
|
66
113
|
* second call has nothing left to do.
|
|
67
114
|
*/
|
|
68
115
|
export function openEngine(options: EngineOptions): Engine {
|
|
69
|
-
const
|
|
116
|
+
const { components } = options;
|
|
117
|
+
let mapper: TemporaryDirectory | undefined;
|
|
118
|
+
if (components !== undefined && components.extensions.length > 0) {
|
|
119
|
+
const made = components.host.temporaryDirectory();
|
|
120
|
+
try {
|
|
121
|
+
components.host.writeDocument(joinPath(made.path, "package.json"), [
|
|
122
|
+
mapperManifest(components.host.componentMapperCommand()),
|
|
123
|
+
]);
|
|
124
|
+
} catch (error: unknown) {
|
|
125
|
+
made.remove();
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
mapper = made;
|
|
129
|
+
}
|
|
130
|
+
const api = new API({
|
|
131
|
+
collectTiming: options.collectTiming,
|
|
132
|
+
...(mapper === undefined ? {} : { runExternalCode: true }),
|
|
133
|
+
});
|
|
70
134
|
let closed = false;
|
|
71
135
|
return {
|
|
72
136
|
parseConfigFile: (file) => api.parseConfigFile(file),
|
|
73
|
-
|
|
137
|
+
parseConfigJson: (json, configFile) =>
|
|
138
|
+
api.parseJsonConfigFileContent(json, { configFileName: configFile }),
|
|
139
|
+
createSnapshot: (openProjects, createPrograms, configurations = new Map()) =>
|
|
140
|
+
api.createSnapshot({
|
|
141
|
+
openProjects: [...openProjects],
|
|
142
|
+
...(createPrograms === undefined ? {} : { createPrograms: [...createPrograms] }),
|
|
143
|
+
...(components === undefined
|
|
144
|
+
? configurations.size === 0
|
|
145
|
+
? {}
|
|
146
|
+
: { fileSystem: { kind: "layer", files: Object.fromEntries(configurations) } }
|
|
147
|
+
: {
|
|
148
|
+
fileSystem: componentLayer(
|
|
149
|
+
components.host,
|
|
150
|
+
openProjects,
|
|
151
|
+
mapper === undefined
|
|
152
|
+
? undefined
|
|
153
|
+
: { extensions: components.extensions, packageDirectory: mapper.path },
|
|
154
|
+
configurations,
|
|
155
|
+
),
|
|
156
|
+
}),
|
|
157
|
+
}),
|
|
158
|
+
readsComponents: mapper !== undefined,
|
|
159
|
+
createModuleResolver: (options, entries) =>
|
|
160
|
+
api.createModuleResolver(options, {
|
|
161
|
+
moduleResolutions: { fallback: "resolve", entries: [...entries] },
|
|
162
|
+
}),
|
|
163
|
+
parseSourceFile: (fileName, text) => {
|
|
164
|
+
const retained = api.createSourceFile(fileName, text);
|
|
165
|
+
try {
|
|
166
|
+
return retained.sourceFile;
|
|
167
|
+
} finally {
|
|
168
|
+
retained.dispose();
|
|
169
|
+
}
|
|
170
|
+
},
|
|
74
171
|
batch: <T>(questions: readonly APIRequestGenerator<T>[]) =>
|
|
75
172
|
api.batch<readonly APIRequestGenerator<T>[]>(...questions),
|
|
76
173
|
ask: (_accessor, _locations, question) => question(),
|
|
@@ -80,7 +177,11 @@ export function openEngine(options: EngineOptions): Engine {
|
|
|
80
177
|
return;
|
|
81
178
|
}
|
|
82
179
|
closed = true;
|
|
83
|
-
|
|
180
|
+
try {
|
|
181
|
+
api.close();
|
|
182
|
+
} finally {
|
|
183
|
+
mapper?.remove();
|
|
184
|
+
}
|
|
84
185
|
},
|
|
85
186
|
};
|
|
86
187
|
}
|
|
@@ -112,6 +213,11 @@ export interface ProjectView<Brand> {
|
|
|
112
213
|
* trip, and one may go unanswered.
|
|
113
214
|
*/
|
|
114
215
|
readonly queries: Queries;
|
|
216
|
+
/**
|
|
217
|
+
* The files the project's configuration names, as the project reads them, so a
|
|
218
|
+
* component file a mapper reads is among them.
|
|
219
|
+
*/
|
|
220
|
+
readonly rootFiles: readonly string[];
|
|
115
221
|
/** The source files the program holds that are the target's own. */
|
|
116
222
|
ownSourceFiles(): readonly SourceFile[];
|
|
117
223
|
/**
|
|
@@ -166,6 +272,34 @@ export interface ProjectView<Brand> {
|
|
|
166
272
|
*/
|
|
167
273
|
export type ProjectVisitor<Result> = <Brand>(project: ProjectView<Brand>) => Result;
|
|
168
274
|
|
|
275
|
+
/**
|
|
276
|
+
* The source files of one program that `own` admits, in the program's order, none of
|
|
277
|
+
* them one of the compiler's default libraries.
|
|
278
|
+
*/
|
|
279
|
+
function filesOwned(
|
|
280
|
+
program: Program,
|
|
281
|
+
own: (fileName: string, fromExternalLibrary: boolean) => boolean,
|
|
282
|
+
): readonly SourceFile[] {
|
|
283
|
+
return (
|
|
284
|
+
program
|
|
285
|
+
.getSourceFileNames()
|
|
286
|
+
// The metadata decides which files belong to the target, and it is read by
|
|
287
|
+
// name. Fetching each file first and filtering afterwards costs one round
|
|
288
|
+
// trip per library file the answer then discards, and a program holds far
|
|
289
|
+
// more library files than target files.
|
|
290
|
+
.filter((name) => {
|
|
291
|
+
const metadata = program.getSourceFileMetadata(name);
|
|
292
|
+
return (
|
|
293
|
+
metadata !== undefined &&
|
|
294
|
+
!metadata.isDefaultLibrary &&
|
|
295
|
+
own(name, metadata.isFromExternalLibrary)
|
|
296
|
+
);
|
|
297
|
+
})
|
|
298
|
+
.map((name) => program.getSourceFile(name))
|
|
299
|
+
.filter((file): file is SourceFile => file !== undefined)
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
169
303
|
function viewOf<Brand>(
|
|
170
304
|
project: Project,
|
|
171
305
|
configFile: string,
|
|
@@ -186,25 +320,13 @@ function viewOf<Brand>(
|
|
|
186
320
|
let ownFiles: readonly SourceFile[] | undefined;
|
|
187
321
|
let ownKeys: ReadonlySet<string> | undefined;
|
|
188
322
|
const ownSourceFiles = (): readonly SourceFile[] => {
|
|
189
|
-
ownFiles ??= project.program
|
|
190
|
-
.getSourceFileNames()
|
|
191
|
-
// The metadata decides which files belong to the target, and it is read by
|
|
192
|
-
// name. Fetching each file first and filtering afterwards costs one round
|
|
193
|
-
// trip per library file the answer then discards, and a program holds far
|
|
194
|
-
// more library files than target files.
|
|
195
|
-
.filter((name) => {
|
|
196
|
-
const metadata = project.program.getSourceFileMetadata(name);
|
|
197
|
-
return (
|
|
198
|
-
metadata !== undefined && !metadata.isDefaultLibrary && !metadata.isFromExternalLibrary
|
|
199
|
-
);
|
|
200
|
-
})
|
|
201
|
-
.map((name) => project.program.getSourceFile(name))
|
|
202
|
-
.filter((file): file is SourceFile => file !== undefined);
|
|
323
|
+
ownFiles ??= filesOwned(project.program, (_name, fromExternalLibrary) => !fromExternalLibrary);
|
|
203
324
|
return ownFiles;
|
|
204
325
|
};
|
|
205
326
|
return {
|
|
206
327
|
configFile,
|
|
207
328
|
program: project.program,
|
|
329
|
+
rootFiles: project.parsedCommandLine.fileNames,
|
|
208
330
|
queries,
|
|
209
331
|
ownSourceFiles,
|
|
210
332
|
ownPaths: () => {
|
|
@@ -263,6 +385,30 @@ export function narrowedTo<Brand>(
|
|
|
263
385
|
};
|
|
264
386
|
}
|
|
265
387
|
|
|
388
|
+
/**
|
|
389
|
+
* The same project, its own source files the ones `own` admits among every file its
|
|
390
|
+
* program holds, given whether the compiler reached the file as a library's.
|
|
391
|
+
*/
|
|
392
|
+
export function ownedAs<Brand>(
|
|
393
|
+
project: ProjectView<Brand>,
|
|
394
|
+
own: (fileName: string, fromExternalLibrary: boolean) => boolean,
|
|
395
|
+
): ProjectView<Brand> {
|
|
396
|
+
let owned: readonly SourceFile[] | undefined;
|
|
397
|
+
let keys: ReadonlySet<string> | undefined;
|
|
398
|
+
const ownSourceFiles = (): readonly SourceFile[] => {
|
|
399
|
+
owned ??= filesOwned(project.program, own);
|
|
400
|
+
return owned;
|
|
401
|
+
};
|
|
402
|
+
return {
|
|
403
|
+
...project,
|
|
404
|
+
ownSourceFiles,
|
|
405
|
+
ownPaths: () => {
|
|
406
|
+
keys ??= new Set(ownSourceFiles().map((file) => file.path));
|
|
407
|
+
return keys;
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
266
412
|
/** Every diagnostic set a project is refused for carrying an error in. */
|
|
267
413
|
export interface ProjectDiagnostics {
|
|
268
414
|
readonly configFile: string;
|
|
@@ -309,33 +455,68 @@ export interface SessionResult<Result> {
|
|
|
309
455
|
type Unopened = "refuse" | "record";
|
|
310
456
|
|
|
311
457
|
/**
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
458
|
+
* The projects one snapshot holds for the configurations named, by configuration file. A
|
|
459
|
+
* project opened from a written configuration is the project of the configuration
|
|
460
|
+
* `written` maps it to.
|
|
461
|
+
*/
|
|
462
|
+
function projectsOf(
|
|
463
|
+
snapshot: Snapshot,
|
|
464
|
+
built: readonly string[],
|
|
465
|
+
written: ReadonlyMap<string, string>,
|
|
466
|
+
): ReadonlyMap<string, Project> {
|
|
467
|
+
const opened = new Map<string, Project>();
|
|
468
|
+
for (const project of snapshot.getProjects()) {
|
|
469
|
+
if (project.configFileName !== undefined) {
|
|
470
|
+
opened.set(written.get(project.configFileName) ?? project.configFileName, project);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
(snapshot.operation.createdPrograms ?? []).forEach((program, at) => {
|
|
474
|
+
const project = snapshot.getProject(program.id);
|
|
475
|
+
const configFile = built[at];
|
|
476
|
+
if (project !== undefined && configFile !== undefined) {
|
|
477
|
+
opened.set(configFile, project);
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
return opened;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Runs `visit` over every project the compiler configurations name, inside one never updated
|
|
485
|
+
* snapshot of one client, and answers in their order. A configuration `programs` describes is
|
|
486
|
+
* built as that program, or opened from the configuration it writes, instead of as a project.
|
|
487
|
+
* One the snapshot holds nothing for ends the run under `"refuse"` and is named in the result
|
|
488
|
+
* under `"record"`. The snapshot is disposed after the last visit and the client released
|
|
489
|
+
* after it, whether the visit completed or threw.
|
|
323
490
|
*/
|
|
324
491
|
export function runSession<Result>(
|
|
325
492
|
engine: Engine,
|
|
326
493
|
configFiles: readonly string[],
|
|
327
494
|
visit: ProjectVisitor<Result>,
|
|
328
495
|
unopened: Unopened = "refuse",
|
|
496
|
+
programs: ReadonlyMap<string, WorkspaceProgram> = new Map(),
|
|
329
497
|
): SessionResult<Result> {
|
|
330
498
|
try {
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
499
|
+
const built: string[] = [];
|
|
500
|
+
const params: CreateSnapshotProgramParams[] = [];
|
|
501
|
+
const written = new Map<string, string>();
|
|
502
|
+
const texts = new Map<string, string>();
|
|
503
|
+
for (const configFile of configFiles) {
|
|
504
|
+
const program = programs.get(configFile);
|
|
505
|
+
if (program?.kind === "program") {
|
|
506
|
+
built.push(configFile);
|
|
507
|
+
params.push(program.params);
|
|
508
|
+
} else if (program?.kind === "configuration") {
|
|
509
|
+
written.set(program.file, configFile);
|
|
510
|
+
texts.set(program.file, program.text);
|
|
338
511
|
}
|
|
512
|
+
}
|
|
513
|
+
const snapshot = engine.createSnapshot(
|
|
514
|
+
[...configFiles.filter((configFile) => !programs.has(configFile)), ...written.keys()],
|
|
515
|
+
params.length === 0 ? undefined : params,
|
|
516
|
+
texts,
|
|
517
|
+
);
|
|
518
|
+
try {
|
|
519
|
+
const opened = projectsOf(snapshot, built, written);
|
|
339
520
|
const log = new UnansweredLog();
|
|
340
521
|
const projects: Result[] = [];
|
|
341
522
|
const missing: string[] = [];
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
|
|
7
|
+
/** The classes of setup failure this analyzer meets. */
|
|
8
|
+
type SetupClass =
|
|
9
|
+
| "missing-module"
|
|
10
|
+
| "missing-consumer"
|
|
11
|
+
| "workspace-member-without-source"
|
|
12
|
+
| "convention-not-literal";
|
|
13
|
+
|
|
14
|
+
export interface SetupFailure {
|
|
15
|
+
readonly setupClass: SetupClass;
|
|
16
|
+
/** What is missing and the fix, on one line. */
|
|
17
|
+
readonly detail: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The failure as the one line of the error stream that names it. */
|
|
21
|
+
export function setupLine(failure: SetupFailure): string {
|
|
22
|
+
return `setup failure: ${failure.setupClass}: ${failure.detail}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A run that met a setup failure where none may be dropped: in a configuration the
|
|
27
|
+
* configuration document or the scope declares, in a declared consumer, or in every
|
|
28
|
+
* configuration discovery derived. It ends the run with the failure code and no finding
|
|
29
|
+
* list.
|
|
30
|
+
*/
|
|
31
|
+
export class SetupError extends Error {
|
|
32
|
+
readonly failures: readonly SetupFailure[];
|
|
33
|
+
|
|
34
|
+
constructor(failures: readonly SetupFailure[]) {
|
|
35
|
+
super(failures.map(setupLine).join("\n"));
|
|
36
|
+
this.name = "SetupError";
|
|
37
|
+
this.failures = failures;
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/source-files.ts
CHANGED
|
@@ -5,53 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { SourceFile } from "@typescript/native/unstable/ast";
|
|
8
|
-
import {
|
|
8
|
+
import { commonDirectory, emittedFrom } from "./emit-map.ts";
|
|
9
|
+
import { dirnamePath, relativePath, resolvePath } from "./paths.ts";
|
|
9
10
|
import type { ProjectView } from "./session.ts";
|
|
10
11
|
|
|
11
|
-
/** The source extensions the compiler emits each output extension from. */
|
|
12
|
-
const EMITTED: readonly { readonly emitted: string; readonly sources: readonly string[] }[] = [
|
|
13
|
-
{ emitted: ".d.ts", sources: [".ts", ".tsx"] },
|
|
14
|
-
{ emitted: ".d.mts", sources: [".mts"] },
|
|
15
|
-
{ emitted: ".d.cts", sources: [".cts"] },
|
|
16
|
-
{ emitted: ".js", sources: [".ts", ".tsx"] },
|
|
17
|
-
{ emitted: ".jsx", sources: [".tsx"] },
|
|
18
|
-
{ emitted: ".mjs", sources: [".mts"] },
|
|
19
|
-
{ emitted: ".cjs", sources: [".cts"] },
|
|
20
|
-
];
|
|
21
|
-
|
|
22
12
|
/** The one character a subpath pattern of a manifest's `exports` holds. */
|
|
23
13
|
const SUBPATH_WILDCARD = "*";
|
|
24
14
|
|
|
25
|
-
/** One path with its extension replaced, where it carries the one named. */
|
|
26
|
-
function reExtended(path: string, emitted: string, source: string): string | undefined {
|
|
27
|
-
return path.endsWith(emitted) ? path.slice(0, path.length - emitted.length) + source : undefined;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* The longest directory every path shares, which is the directory the compiler
|
|
32
|
-
* emits from where the configuration declares no root directory.
|
|
33
|
-
*/
|
|
34
|
-
function commonDirectory(paths: readonly string[]): string {
|
|
35
|
-
const first = paths[0];
|
|
36
|
-
if (first === undefined) {
|
|
37
|
-
return "";
|
|
38
|
-
}
|
|
39
|
-
let common = dirnamePath(first).split("/");
|
|
40
|
-
for (const path of paths.slice(1)) {
|
|
41
|
-
const segments = dirnamePath(path).split("/");
|
|
42
|
-
let shared = 0;
|
|
43
|
-
while (
|
|
44
|
-
shared < common.length &&
|
|
45
|
-
shared < segments.length &&
|
|
46
|
-
common[shared] === segments[shared]
|
|
47
|
-
) {
|
|
48
|
-
shared += 1;
|
|
49
|
-
}
|
|
50
|
-
common = common.slice(0, shared);
|
|
51
|
-
}
|
|
52
|
-
return common.join("/");
|
|
53
|
-
}
|
|
54
|
-
|
|
55
15
|
/**
|
|
56
16
|
* Whether one path is what a subpath pattern stands for: the text before the
|
|
57
17
|
* wildcard, any run of characters including the solidus, then the text after it.
|
|
@@ -108,32 +68,10 @@ export function sourceFilesOf<Brand>(project: ProjectView<Brand>, targetRoot: st
|
|
|
108
68
|
const declarationDir = at(options.declarationDir);
|
|
109
69
|
const sourceDir = at(options.rootDir) ?? commonDirectory([...byName.keys()]);
|
|
110
70
|
|
|
111
|
-
|
|
112
|
-
const fromEmit = (path: string, emitted: string): string | undefined => {
|
|
113
|
-
const emitDir = emitted.startsWith(".d.") ? (declarationDir ?? outDir) : outDir;
|
|
114
|
-
if (emitDir === undefined) {
|
|
115
|
-
return undefined;
|
|
116
|
-
}
|
|
117
|
-
const below = relativePath(emitDir, path);
|
|
118
|
-
return below === undefined ? undefined : joinPath(sourceDir, below);
|
|
119
|
-
};
|
|
71
|
+
const layout = { outDir, declarationDir, sourceDirs: [sourceDir] };
|
|
120
72
|
|
|
121
73
|
/** The source paths one written path may stand for, the path itself first. */
|
|
122
|
-
const candidates = (path: string): readonly string[] =>
|
|
123
|
-
const found = [path];
|
|
124
|
-
for (const { emitted, sources } of EMITTED) {
|
|
125
|
-
const mapped = fromEmit(path, emitted);
|
|
126
|
-
for (const source of sources) {
|
|
127
|
-
for (const from of [path, mapped]) {
|
|
128
|
-
const candidate = from === undefined ? undefined : reExtended(from, emitted, source);
|
|
129
|
-
if (candidate !== undefined) {
|
|
130
|
-
found.push(candidate);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return found;
|
|
136
|
-
};
|
|
74
|
+
const candidates = (path: string): readonly string[] => [path, ...emittedFrom(path, layout)];
|
|
137
75
|
|
|
138
76
|
return {
|
|
139
77
|
byPath,
|
package/src/sweep.ts
CHANGED
|
@@ -22,6 +22,11 @@ export interface Exemption {
|
|
|
22
22
|
readonly detail: string;
|
|
23
23
|
/** Where the evidence is written. */
|
|
24
24
|
readonly site: Position;
|
|
25
|
+
/**
|
|
26
|
+
* The component file, below the target root, whose markup holds the evidence. The
|
|
27
|
+
* record holds only while that file is live.
|
|
28
|
+
*/
|
|
29
|
+
readonly whileLive?: string;
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
/**
|
|
@@ -253,13 +258,15 @@ function admitTestsOfDeadCode(
|
|
|
253
258
|
): readonly boolean[] {
|
|
254
259
|
const admitted = graph.symbols.map(() => false);
|
|
255
260
|
graph.symbols.forEach((_symbol, at) => {
|
|
256
|
-
|
|
261
|
+
const testCode = graph.test[at] === true || graph.support[at] === true;
|
|
262
|
+
if (!testCode || graph.subject[at] !== true || held[at] === true) {
|
|
257
263
|
return;
|
|
258
264
|
}
|
|
259
265
|
let targets = 0;
|
|
260
266
|
let live = 0;
|
|
261
267
|
for (const edge of graph.out[at] ?? []) {
|
|
262
|
-
|
|
268
|
+
const target = graph.test[edge.to] === true || graph.support[edge.to] === true;
|
|
269
|
+
if (target || graph.subject[edge.to] !== true) {
|
|
263
270
|
continue;
|
|
264
271
|
}
|
|
265
272
|
targets += 1;
|
package/src/template-field.ts
CHANGED
|
@@ -12,13 +12,14 @@
|
|
|
12
12
|
* type, because nothing ties the name to a type.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { readComponent } from "./component-files.ts";
|
|
15
16
|
import { ConfigError, type Analysis, type TemplateDelimiters } from "./config.ts";
|
|
16
17
|
import type { DetectorInput, Evidence } from "./exempt.ts";
|
|
17
18
|
import type { Host } from "./host.ts";
|
|
18
19
|
import type { SymbolKind } from "./inventory.ts";
|
|
19
20
|
import { memberNames } from "./member-names.ts";
|
|
20
|
-
import { isAbsolutePath, joinPath, normalizePath } from "./paths.ts";
|
|
21
|
-
import type
|
|
21
|
+
import { isAbsolutePath, joinPath, normalizePath, relativePath } from "./paths.ts";
|
|
22
|
+
import { isComponentFile, type Position } from "./position.ts";
|
|
22
23
|
|
|
23
24
|
/** One template, by its path below the target root. */
|
|
24
25
|
export interface TemplateFile {
|
|
@@ -40,6 +41,12 @@ const READ_BY_A_TEMPLATE: ReadonlySet<SymbolKind> = new Set([
|
|
|
40
41
|
"type-member",
|
|
41
42
|
]);
|
|
42
43
|
|
|
44
|
+
/**
|
|
45
|
+
* The kinds of declaration a component file's markup can name. Its expressions are the
|
|
46
|
+
* language's own, so a static member and an enum member are reachable by name too.
|
|
47
|
+
*/
|
|
48
|
+
const READ_BY_MARKUP: ReadonlySet<SymbolKind> = new Set([...READ_BY_A_TEMPLATE, "enum-member"]);
|
|
49
|
+
|
|
43
50
|
/** One identifier, as the grammar of the analyzed language spells one. */
|
|
44
51
|
const IDENTIFIER = /[\p{ID_Start}$_][\p{ID_Continue}$\u200C\u200D]*/uy;
|
|
45
52
|
|
|
@@ -173,6 +180,17 @@ function namesOf(text: string, delimiters: TemplateDelimiters): readonly Named[]
|
|
|
173
180
|
}
|
|
174
181
|
}
|
|
175
182
|
|
|
183
|
+
/** Every identifier the markup of one component file writes in an action. */
|
|
184
|
+
function markupNamesOf(text: string): readonly Named[] {
|
|
185
|
+
return readComponent(text).markup.flatMap((one) =>
|
|
186
|
+
identifiersOf(one.text).map(({ name, offset }) => ({
|
|
187
|
+
name,
|
|
188
|
+
offset: one.start + offset,
|
|
189
|
+
action: one.action,
|
|
190
|
+
})),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
176
194
|
/** Where one offset of a template is, its column counted in UTF-16 code units. */
|
|
177
195
|
function positionIn(file: TemplateFile, offset: number): Position {
|
|
178
196
|
const before = file.text.slice(0, offset);
|
|
@@ -187,25 +205,42 @@ function positionIn(file: TemplateFile, offset: number): Position {
|
|
|
187
205
|
/**
|
|
188
206
|
* The template-field detector: every instance member of a class and every member of
|
|
189
207
|
* an interface or an object type whose name an action of a configured template
|
|
190
|
-
* writes,
|
|
208
|
+
* writes, and every member of those and of an enum whose name a component file's
|
|
209
|
+
* markup writes, recorded at the identifier and held while that file is live.
|
|
191
210
|
*/
|
|
192
211
|
export function templateField<Brand>(input: DetectorInput<Brand>): readonly Evidence[] {
|
|
193
212
|
const { templates } = input;
|
|
194
|
-
|
|
213
|
+
const components = input.project.ownSourceFiles().flatMap((file) => {
|
|
214
|
+
const path = isComponentFile(file) ? relativePath(input.targetRoot, file.fileName) : undefined;
|
|
215
|
+
return path === undefined ? [] : [{ path, text: file.originalText }];
|
|
216
|
+
});
|
|
217
|
+
if (templates.files.length === 0 && components.length === 0) {
|
|
195
218
|
return [];
|
|
196
219
|
}
|
|
197
220
|
const members = memberNames(input.project.ownSourceFiles(), input.held);
|
|
198
221
|
const found: Evidence[] = [];
|
|
199
|
-
|
|
200
|
-
|
|
222
|
+
const scanned = [
|
|
223
|
+
...templates.files.map((file) => ({
|
|
224
|
+
file,
|
|
225
|
+
names: namesOf(file.text, templates.delimiters),
|
|
226
|
+
markup: false,
|
|
227
|
+
})),
|
|
228
|
+
...components.map((file) => ({ file, names: markupNamesOf(file.text), markup: true })),
|
|
229
|
+
];
|
|
230
|
+
for (const { file, names, markup } of scanned) {
|
|
231
|
+
for (const named of names) {
|
|
201
232
|
for (const member of members.get(named.name) ?? []) {
|
|
202
|
-
|
|
233
|
+
const readable = markup
|
|
234
|
+
? READ_BY_MARKUP.has(member.kind)
|
|
235
|
+
: READ_BY_A_TEMPLATE.has(member.kind) && !member.static;
|
|
236
|
+
if (!readable) {
|
|
203
237
|
continue;
|
|
204
238
|
}
|
|
205
239
|
found.push({
|
|
206
240
|
id: member.id,
|
|
207
241
|
detail: `named by ${named.action}`,
|
|
208
242
|
site: positionIn(file, named.offset),
|
|
243
|
+
...(markup ? { whileLive: file.path } : {}),
|
|
209
244
|
});
|
|
210
245
|
}
|
|
211
246
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
|
|
8
|
+
import type { InventorySymbol } from "./inventory.ts";
|
|
9
|
+
import type { Reference } from "./references.ts";
|
|
10
|
+
import type { Root } from "./roots.ts";
|
|
11
|
+
|
|
12
|
+
/** The root kind that never takes a file out of test-support code: the skip only withholds. */
|
|
13
|
+
const SKIP_ROOT = "type-error";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The files of one project that are test-support code, below the target root. A file
|
|
17
|
+
* holding a root is not one, because a root has a caller outside the import graph: a
|
|
18
|
+
* runtime, a tool, a configuration, or a consumer of a library's published API.
|
|
19
|
+
* `testFiles` are the files the test-file rules classified.
|
|
20
|
+
*/
|
|
21
|
+
export function testSupportFiles(
|
|
22
|
+
symbols: readonly InventorySymbol[],
|
|
23
|
+
references: readonly Reference[],
|
|
24
|
+
roots: readonly Root[],
|
|
25
|
+
testFiles: readonly string[],
|
|
26
|
+
): ReadonlySet<string> {
|
|
27
|
+
const fileOf = new Map(symbols.map((symbol) => [symbol.id, symbol.position.path]));
|
|
28
|
+
const users = new Map<string, Set<string>>();
|
|
29
|
+
for (const reference of references) {
|
|
30
|
+
const used = fileOf.get(reference.to);
|
|
31
|
+
const from = reference.position.path;
|
|
32
|
+
if (reference.consumer !== undefined || used === undefined || used === from) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const held = users.get(used);
|
|
36
|
+
if (held === undefined) {
|
|
37
|
+
users.set(used, new Set([from]));
|
|
38
|
+
} else {
|
|
39
|
+
held.add(from);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const tests = new Set(testFiles);
|
|
43
|
+
const rooted = new Set(
|
|
44
|
+
roots.filter((root) => root.kind !== SKIP_ROOT).flatMap((root) => fileOf.get(root.id) ?? []),
|
|
45
|
+
);
|
|
46
|
+
const candidates = [...users.keys()].filter((path) => !tests.has(path) && !rooted.has(path));
|
|
47
|
+
const support = new Set<string>();
|
|
48
|
+
for (let changed = true; changed;) {
|
|
49
|
+
changed = false;
|
|
50
|
+
for (const path of candidates) {
|
|
51
|
+
if (support.has(path)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const from = users.get(path) ?? new Set<string>();
|
|
55
|
+
if ([...from].every((one) => tests.has(one) || support.has(one))) {
|
|
56
|
+
support.add(path);
|
|
57
|
+
changed = true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return support;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The references with every one a test-support file makes counted as a test reference. */
|
|
65
|
+
export function supportReferences(
|
|
66
|
+
references: readonly Reference[],
|
|
67
|
+
support: ReadonlySet<string>,
|
|
68
|
+
): readonly Reference[] {
|
|
69
|
+
if (support.size === 0) {
|
|
70
|
+
return references;
|
|
71
|
+
}
|
|
72
|
+
return references.map((reference) =>
|
|
73
|
+
!reference.test && support.has(reference.position.path)
|
|
74
|
+
? { ...reference, test: true }
|
|
75
|
+
: reference,
|
|
76
|
+
);
|
|
77
|
+
}
|