@cplieger/deadset-ts 1.0.0 → 1.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/src/session.ts ADDED
@@ -0,0 +1,221 @@
1
+ import { API } from "@typescript/native/unstable/sync";
2
+ import type {
3
+ Checker,
4
+ Diagnostic,
5
+ DocumentIdentifier,
6
+ NodeHandle,
7
+ Project,
8
+ Program,
9
+ Snapshot,
10
+ Symbol as TSSymbol,
11
+ TimingInfo,
12
+ } from "@typescript/native/unstable/sync";
13
+ import { SyntaxKind } from "@typescript/native/unstable/ast";
14
+ import type { Identifier, Node, SourceFile } from "@typescript/native/unstable/ast";
15
+
16
+ /**
17
+ * The compiler the analysis reads through. It is a client over a protocol to a
18
+ * separate process, so every accessor call is a round trip and a run holds one
19
+ * client and one snapshot.
20
+ *
21
+ * It is an interface because the run's own accounting for that lifetime is not on
22
+ * the client's surface: a snapshot reports whether it is disposed and not how many
23
+ * a client opened, so the count is observed at this seam.
24
+ */
25
+ export interface Engine {
26
+ /**
27
+ * Resolves one compiler configuration file, or throws with the reason it cannot
28
+ * be read. The result is spelled through the client's own signature: the type
29
+ * the compiler package returns here is not one of its exported names.
30
+ */
31
+ parseConfigFile(file: DocumentIdentifier): ReturnType<API["parseConfigFile"]>;
32
+ /** Opens one snapshot holding every project named. */
33
+ updateSnapshot(openProjects: readonly string[]): Snapshot;
34
+ /** Round-trip latency, bytes transferred and server time, when collection is on. */
35
+ getTimingInfo(): TimingInfo;
36
+ /** Releases the client and the process behind it. */
37
+ close(): void;
38
+ }
39
+
40
+ /** How a run opens its compiler client. */
41
+ export interface EngineOptions {
42
+ /** Collect per-request timing, which the calibration record reads. */
43
+ readonly collectTiming: boolean;
44
+ }
45
+
46
+ /**
47
+ * Opens the compiler client the analysis reads through.
48
+ *
49
+ * Closing twice releases once: a run that fails before its snapshot opens closes
50
+ * the client where it failed, and the outer release runs whatever happened, so the
51
+ * second call has nothing left to do.
52
+ */
53
+ export function openEngine(options: EngineOptions): Engine {
54
+ const api = new API({ collectTiming: options.collectTiming });
55
+ let closed = false;
56
+ return {
57
+ parseConfigFile: (file) => api.parseConfigFile(file),
58
+ updateSnapshot: (openProjects) => api.updateSnapshot({ openProjects: [...openProjects] }),
59
+ getTimingInfo: () => api.getTimingInfo(),
60
+ close: () => {
61
+ if (closed) {
62
+ return;
63
+ }
64
+ closed = true;
65
+ api.close();
66
+ },
67
+ };
68
+ }
69
+
70
+ /**
71
+ * A node of one project's program, carried with that project's identity.
72
+ *
73
+ * `Brand` is a type parameter and never a value: a view is handed to a visitor
74
+ * that introduces its own `Brand`, so two projects' views have handle types the
75
+ * compiler will not exchange and a handle cannot reach another project's checker.
76
+ * Handles are only meaningful inside the program that produced them, and the
77
+ * cross-project step of the analysis compares rendered positions rather than
78
+ * handles.
79
+ */
80
+ export interface Handle<Brand> {
81
+ readonly node: Node;
82
+ /** Present only in the type: the project this handle belongs to. */
83
+ readonly project?: Brand;
84
+ }
85
+
86
+ /** One project's program and checker, together with the handles they accept. */
87
+ export interface ProjectView<Brand> {
88
+ /** The compiler configuration file this project was opened from. */
89
+ readonly configFile: string;
90
+ /** The project's program, whose walk is local to this process. */
91
+ readonly program: Program;
92
+ /** The project's checker. Every call on it is a round trip. */
93
+ readonly checker: Checker;
94
+ /** The source files the program holds that are the target's own. */
95
+ ownSourceFiles(): readonly SourceFile[];
96
+ /** A handle to one node of this project's program. */
97
+ handle(node: Node): Handle<Brand>;
98
+ /**
99
+ * The symbols a batch of this project's nodes resolve to, one round trip for
100
+ * the batch, in the order the handles were given. The batch is how a file's
101
+ * identifiers reach the checker, and `undefined` marks a node the batch did not
102
+ * resolve.
103
+ */
104
+ symbolsAt(handles: readonly Handle<Brand>[]): readonly (TSSymbol | undefined)[];
105
+ /**
106
+ * The node one symbol's declaration handle names, read in this project's program,
107
+ * or `undefined` where this project's program does not hold that file.
108
+ *
109
+ * A symbol is shared by every project of the snapshot and remembers the project it
110
+ * was first seen in, so resolving its handle without saying which program to read
111
+ * would answer out of whichever project reached the symbol first. The project is
112
+ * named here instead, and the answer becomes a handle of this project like any
113
+ * other. The read is local once the file has been fetched.
114
+ */
115
+ declarationAt(handle: NodeHandle): Handle<Brand> | undefined;
116
+ }
117
+
118
+ /**
119
+ * What a run does with one project. It carries its own type parameter so each
120
+ * invocation gets a distinct `Brand`: a handle taken from one invocation's view is
121
+ * not a handle another invocation's view accepts.
122
+ */
123
+ export type ProjectVisitor<Result> = <Brand>(project: ProjectView<Brand>) => Result;
124
+
125
+ /**
126
+ * The identifiers of one source file, in source order. A file is the unit a batch
127
+ * resolves: the walk is local to this process and the resolution is not, so one
128
+ * file's identifiers are gathered here and handed to the checker together.
129
+ */
130
+ export function identifiersOf(file: SourceFile): Identifier[] {
131
+ const found: Identifier[] = [];
132
+ const visit = (node: Node): void => {
133
+ if (node.kind === SyntaxKind.Identifier) {
134
+ found.push(node as Identifier);
135
+ }
136
+ node.forEachChild(visit);
137
+ };
138
+ file.forEachChild(visit);
139
+ return found;
140
+ }
141
+
142
+ function viewOf<Brand>(project: Project): ProjectView<Brand> {
143
+ return {
144
+ configFile: project.configFileName,
145
+ program: project.program,
146
+ checker: project.checker,
147
+ ownSourceFiles: () =>
148
+ project.program
149
+ .getSourceFileNames()
150
+ // The metadata decides which files belong to the target, and it is read by
151
+ // name. Fetching each file first and filtering afterwards costs one round
152
+ // trip per library file the answer then discards, and a program holds far
153
+ // more library files than target files.
154
+ .filter((name) => {
155
+ const metadata = project.program.getSourceFileMetadata(name);
156
+ return (
157
+ metadata !== undefined && !metadata.isDefaultLibrary && !metadata.isFromExternalLibrary
158
+ );
159
+ })
160
+ .map((name) => project.program.getSourceFile(name))
161
+ .filter((file): file is SourceFile => file !== undefined),
162
+ handle: (node) => ({ node }),
163
+ symbolsAt: (handles) => project.checker.getSymbolAtLocation(handles.map(({ node }) => node)),
164
+ declarationAt: (handle) => {
165
+ const node = handle.resolve(project);
166
+ return node === undefined ? undefined : { node };
167
+ },
168
+ };
169
+ }
170
+
171
+ /** Every diagnostic set a project is refused for carrying an error in. */
172
+ export interface ProjectDiagnostics {
173
+ readonly configFile: string;
174
+ readonly syntactic: readonly Diagnostic[];
175
+ readonly semantic: readonly Diagnostic[];
176
+ readonly configParsing: readonly Diagnostic[];
177
+ }
178
+
179
+ /** The three diagnostic sets one project carries, read before any analysis. */
180
+ export function diagnosticsOf<Brand>(project: ProjectView<Brand>): ProjectDiagnostics {
181
+ return {
182
+ configFile: project.configFile,
183
+ syntactic: project.program.getSyntacticDiagnostics(),
184
+ semantic: project.program.getSemanticDiagnostics(),
185
+ configParsing: project.program.getConfigFileParsingDiagnostics(),
186
+ };
187
+ }
188
+
189
+ /** One run's result: what the visitor answered for each project, and what it cost. */
190
+ export interface SessionResult<Result> {
191
+ readonly projects: readonly Result[];
192
+ readonly timing: TimingInfo;
193
+ }
194
+
195
+ /**
196
+ * Runs `visit` over every project the compiler configurations name, inside one
197
+ * snapshot of one client.
198
+ *
199
+ * The snapshot is opened once with every configuration, so each becomes a project
200
+ * with its own program and checker; it is never updated, because the analysis
201
+ * reads and the tree does not change under it. It is disposed once when the last
202
+ * project has been visited, and the client is released after it, whether the visit
203
+ * completed or threw.
204
+ */
205
+ export function runSession<Result>(
206
+ engine: Engine,
207
+ configFiles: readonly string[],
208
+ visit: ProjectVisitor<Result>,
209
+ ): SessionResult<Result> {
210
+ try {
211
+ const snapshot = engine.updateSnapshot(configFiles);
212
+ try {
213
+ const projects = snapshot.getProjects().map((project) => visit(viewOf(project)));
214
+ return { projects, timing: engine.getTimingInfo() };
215
+ } finally {
216
+ snapshot.dispose();
217
+ }
218
+ } finally {
219
+ engine.close();
220
+ }
221
+ }
package/src/version.ts ADDED
@@ -0,0 +1,22 @@
1
+ import manifest from "../package.json" with { type: "json" };
2
+
3
+ /**
4
+ * The Contract version this analyzer implements. It is the resolved value of
5
+ * `contract_version` when no configuration source supplies one, and it moves
6
+ * independently of {@link version}.
7
+ */
8
+ export const CONTRACT_VERSION = "1.6.0";
9
+
10
+ /**
11
+ * This analyzer's own version, which the published package carries rather than
12
+ * the source: the release pipeline writes the released version into the manifest
13
+ * of the artifact it publishes, so an installed package reports the release it
14
+ * came from while a checkout reports the version the manifest holds in git.
15
+ *
16
+ * It is one function because a version written in more than one place is two
17
+ * facts that can disagree: the version verb, the report envelope and anything
18
+ * else that names this analyzer read this one.
19
+ */
20
+ export function version(): string {
21
+ return manifest.version;
22
+ }