@cplieger/deadset-ts 3.12.1 → 4.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/node-host.js +10 -7
- package/dist/src/alias-chain.js +59 -7
- package/dist/src/analysis.js +217 -48
- package/dist/src/calls.js +23 -22
- package/dist/src/class-members.js +10 -11
- package/dist/src/components.js +4 -2
- package/dist/src/configured-declarations.js +56 -7
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/dependencies.js +34 -17
- package/dist/src/deprecation.js +35 -24
- package/dist/src/destructuring.js +37 -29
- package/dist/src/discover.js +105 -28
- package/dist/src/edges.js +4 -1
- package/dist/src/entry-points.js +7 -0
- package/dist/src/enum-group.js +46 -40
- package/dist/src/exempt.js +42 -1
- package/dist/src/findings/completion.js +37 -8
- package/dist/src/findings/emitters.js +20 -6
- package/dist/src/findings/interfaces.js +7 -1
- package/dist/src/findings/intra-function.js +91 -2
- package/dist/src/findings/self-check.js +30 -7
- package/dist/src/findings/unused-declarations.js +15 -3
- package/dist/src/findings-pass.js +18 -11
- package/dist/src/framework-lifecycle.js +8 -6
- package/dist/src/implementations.js +45 -23
- package/dist/src/interface-satisfaction.js +30 -27
- package/dist/src/intra-function-parts.js +660 -0
- package/dist/src/inventory.js +85 -4
- package/dist/src/json-chunks.js +76 -0
- package/dist/src/kinds.js +337 -35
- package/dist/src/partial.js +23 -0
- package/dist/src/query.js +207 -0
- package/dist/src/references.js +92 -20
- package/dist/src/reflective-lookup.js +46 -4
- package/dist/src/report.js +14 -3
- package/dist/src/reporters/annotations.js +9 -5
- package/dist/src/reporters/json.js +3 -2
- package/dist/src/reporters/sarif.js +22 -16
- package/dist/src/reporters/template.js +112 -12
- package/dist/src/reporters/text.js +8 -6
- package/dist/src/roots.js +27 -11
- package/dist/src/serialization-contract.js +53 -32
- package/dist/src/session.js +79 -39
- package/dist/src/signatures.js +32 -0
- package/dist/src/suppress-file.js +29 -7
- package/dist/src/suppress-inline.js +8 -1
- package/dist/src/suppress.js +27 -0
- package/dist/src/sweep.js +4 -1
- package/dist/src/value-reach.js +44 -25
- package/dist/src/verbs/analyze.js +39 -14
- package/dist/src/verbs/describe.js +12 -8
- package/dist/src/verbs/explain.js +22 -9
- package/dist/src/verbs/print-projects.js +40 -7
- package/dist/src/verbs/print-retained.js +6 -2
- package/dist/src/verbs/print-roots.js +5 -1
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +3 -3
- package/src/alias-chain.ts +75 -11
- package/src/analysis.ts +385 -104
- package/src/calls.ts +10 -9
- package/src/class-members.ts +10 -11
- package/src/components.ts +4 -2
- package/src/configured-declarations.ts +80 -8
- package/src/conformance-record.ts +2 -2
- package/src/dependencies.ts +42 -20
- package/src/deprecation.ts +53 -27
- package/src/destructuring.ts +50 -39
- package/src/discover.ts +177 -33
- package/src/edges.ts +4 -1
- package/src/entry-points.ts +7 -0
- package/src/enum-group.ts +56 -42
- package/src/exempt.ts +50 -2
- package/src/finding.ts +5 -1
- package/src/findings/completion.ts +46 -10
- package/src/findings/emitter.ts +3 -0
- package/src/findings/emitters.ts +27 -6
- package/src/findings/interfaces.ts +7 -1
- package/src/findings/intra-function.ts +106 -3
- package/src/findings/self-check.ts +48 -7
- package/src/findings/unused-declarations.ts +18 -4
- package/src/findings-pass.ts +26 -11
- package/src/framework-lifecycle.ts +10 -6
- package/src/host.ts +5 -3
- package/src/implementations.ts +59 -23
- package/src/interface-satisfaction.ts +30 -27
- package/src/intra-function-parts.ts +889 -0
- package/src/inventory.ts +97 -4
- package/src/json-chunks.ts +87 -0
- package/src/kinds.ts +355 -41
- package/src/partial.ts +40 -0
- package/src/query.ts +522 -0
- package/src/references.ts +119 -38
- package/src/reflective-lookup.ts +56 -6
- package/src/report.ts +32 -4
- package/src/reporters/annotations.ts +23 -21
- package/src/reporters/json.ts +3 -2
- package/src/reporters/reporter.ts +2 -2
- package/src/reporters/sarif.ts +27 -18
- package/src/reporters/template.ts +119 -14
- package/src/reporters/text.ts +12 -12
- package/src/roots.ts +34 -12
- package/src/serialization-contract.ts +59 -40
- package/src/session.ts +146 -58
- package/src/signatures.ts +55 -0
- package/src/suppress-file.ts +40 -8
- package/src/suppress-inline.ts +8 -1
- package/src/suppress.ts +31 -0
- package/src/sweep.ts +9 -4
- package/src/value-reach.ts +66 -28
- package/src/verbs/analyze.ts +51 -14
- package/src/verbs/describe.ts +13 -9
- package/src/verbs/explain.ts +20 -9
- package/src/verbs/print-projects.ts +53 -7
- package/src/verbs/print-retained.ts +6 -2
- package/src/verbs/print-roots.ts +5 -1
- package/src/version.ts +1 -1
- package/dist/src/reporters/rules.js +0 -178
- package/src/reporters/rules.ts +0 -195
package/src/session.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { API, SymbolFlags } from "@typescript/native/unstable/sync";
|
|
2
2
|
import type {
|
|
3
|
-
|
|
3
|
+
APIRequestGenerator,
|
|
4
4
|
Diagnostic,
|
|
5
5
|
DocumentIdentifier,
|
|
6
6
|
NodeHandle,
|
|
@@ -10,9 +10,15 @@ import type {
|
|
|
10
10
|
Symbol as TSSymbol,
|
|
11
11
|
TimingInfo,
|
|
12
12
|
} from "@typescript/native/unstable/sync";
|
|
13
|
-
import { isIdentifier } from "@typescript/native/unstable/ast";
|
|
14
13
|
import type { Node, SourceFile } from "@typescript/native/unstable/ast";
|
|
15
14
|
import { DiscoveryError } from "./discover.ts";
|
|
15
|
+
import {
|
|
16
|
+
queriesOf,
|
|
17
|
+
UnansweredLog,
|
|
18
|
+
type Answer,
|
|
19
|
+
type Queries,
|
|
20
|
+
type UnansweredQuestion,
|
|
21
|
+
} from "./query.ts";
|
|
16
22
|
|
|
17
23
|
/**
|
|
18
24
|
* The compiler the analysis reads through. It is a client over a protocol to a
|
|
@@ -31,7 +37,15 @@ export interface Engine {
|
|
|
31
37
|
*/
|
|
32
38
|
parseConfigFile(file: DocumentIdentifier): ReturnType<API["parseConfigFile"]>;
|
|
33
39
|
/** Opens one snapshot holding every project named. */
|
|
34
|
-
|
|
40
|
+
createSnapshot(openProjects: readonly string[]): Snapshot;
|
|
41
|
+
/** Sends many questions in one round trip and answers each, in order. */
|
|
42
|
+
batch<T>(questions: readonly APIRequestGenerator<T>[]): T[];
|
|
43
|
+
/**
|
|
44
|
+
* Puts one question to a project's checker, by the accessor's name and the locations
|
|
45
|
+
* it asks about. Every guarded question passes through here, so a caller that
|
|
46
|
+
* observes or fails questions does it at this seam.
|
|
47
|
+
*/
|
|
48
|
+
ask<T>(accessor: string, locations: () => readonly string[], question: () => T): T;
|
|
35
49
|
/** Round-trip latency, bytes transferred and server time, when collection is on. */
|
|
36
50
|
getTimingInfo(): TimingInfo;
|
|
37
51
|
/** Releases the client and the process behind it. */
|
|
@@ -56,7 +70,10 @@ export function openEngine(options: EngineOptions): Engine {
|
|
|
56
70
|
let closed = false;
|
|
57
71
|
return {
|
|
58
72
|
parseConfigFile: (file) => api.parseConfigFile(file),
|
|
59
|
-
|
|
73
|
+
createSnapshot: (openProjects) => api.createSnapshot({ openProjects: [...openProjects] }),
|
|
74
|
+
batch: <T>(questions: readonly APIRequestGenerator<T>[]) =>
|
|
75
|
+
api.batch<readonly APIRequestGenerator<T>[]>(...questions),
|
|
76
|
+
ask: (_accessor, _locations, question) => question(),
|
|
60
77
|
getTimingInfo: () => api.getTimingInfo(),
|
|
61
78
|
close: () => {
|
|
62
79
|
if (closed) {
|
|
@@ -90,38 +107,45 @@ export interface ProjectView<Brand> {
|
|
|
90
107
|
readonly configFile: string;
|
|
91
108
|
/** The project's program, whose walk is local to this process. */
|
|
92
109
|
readonly program: Program;
|
|
93
|
-
/**
|
|
94
|
-
|
|
110
|
+
/**
|
|
111
|
+
* The questions the project's checker answers, each guarded. Every one is a round
|
|
112
|
+
* trip, and one may go unanswered.
|
|
113
|
+
*/
|
|
114
|
+
readonly queries: Queries;
|
|
95
115
|
/** The source files the program holds that are the target's own. */
|
|
96
116
|
ownSourceFiles(): readonly SourceFile[];
|
|
117
|
+
/**
|
|
118
|
+
* The keys of the target's own files, which is how a declaration handle names the
|
|
119
|
+
* file it is in.
|
|
120
|
+
*/
|
|
121
|
+
ownPaths(): ReadonlySet<string>;
|
|
97
122
|
/** A handle to one node of this project's program. */
|
|
98
123
|
handle(node: Node): Handle<Brand>;
|
|
99
124
|
/**
|
|
100
|
-
* The symbols a batch of this project's nodes resolve to,
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* resolve.
|
|
125
|
+
* The symbols a batch of this project's nodes resolve to, in the order the handles
|
|
126
|
+
* were given, in runs of at most `cap`. The batch is how a file's identifiers reach
|
|
127
|
+
* the checker, and `undefined` marks a node the batch did not resolve.
|
|
104
128
|
*/
|
|
105
|
-
symbolsAt(handles: readonly Handle<Brand>[]):
|
|
129
|
+
symbolsAt(handles: readonly Handle<Brand>[], cap?: number): Answer<TSSymbol | undefined>[];
|
|
106
130
|
/**
|
|
107
131
|
* The symbol one node resolves to, asked for that node alone. The accessor behind
|
|
108
|
-
* it takes one identifier and offers no array
|
|
109
|
-
*
|
|
110
|
-
*
|
|
132
|
+
* it takes one identifier and offers no array form, so it answers only for the
|
|
133
|
+
* residue a batch left unresolved. A node that is not an identifier has no such
|
|
134
|
+
* answer.
|
|
111
135
|
*/
|
|
112
|
-
resolvedSymbolAt(handle: Handle<Brand>): TSSymbol | undefined
|
|
136
|
+
resolvedSymbolAt(handle: Handle<Brand>): Answer<TSSymbol | undefined>;
|
|
113
137
|
/**
|
|
114
138
|
* The symbol the value of one shorthand property assignment names, which is the
|
|
115
139
|
* declaration `{ a }` reads; the symbol the property's own name resolves to is the
|
|
116
140
|
* property the literal declares. The handle names the assignment, not its name.
|
|
117
141
|
*/
|
|
118
|
-
shorthandValueAt(handle: Handle<Brand>): TSSymbol | undefined
|
|
142
|
+
shorthandValueAt(handle: Handle<Brand>): Answer<TSSymbol | undefined>;
|
|
119
143
|
/**
|
|
120
144
|
* The symbol one alias names, one link along: an import or export specifier
|
|
121
145
|
* resolves to the declaration it carries forward, or to the next alias of a chain.
|
|
122
146
|
* `undefined` where the symbol is no alias, or where the alias resolves to nothing.
|
|
123
147
|
*/
|
|
124
|
-
aliasStepOf(symbol: TSSymbol): TSSymbol | undefined
|
|
148
|
+
aliasStepOf(symbol: TSSymbol): Answer<TSSymbol | undefined>;
|
|
125
149
|
/**
|
|
126
150
|
* The node one symbol's declaration handle names, read in this project's program,
|
|
127
151
|
* or `undefined` where this project's program does not hold that file.
|
|
@@ -142,49 +166,70 @@ export interface ProjectView<Brand> {
|
|
|
142
166
|
*/
|
|
143
167
|
export type ProjectVisitor<Result> = <Brand>(project: ProjectView<Brand>) => Result;
|
|
144
168
|
|
|
145
|
-
function viewOf<Brand>(
|
|
169
|
+
function viewOf<Brand>(
|
|
170
|
+
project: Project,
|
|
171
|
+
configFile: string,
|
|
172
|
+
engine: Engine,
|
|
173
|
+
log: UnansweredLog,
|
|
174
|
+
): ProjectView<Brand> {
|
|
175
|
+
const queries = queriesOf(
|
|
176
|
+
project.checker,
|
|
177
|
+
configFile,
|
|
178
|
+
(questions) => engine.batch(questions),
|
|
179
|
+
log,
|
|
180
|
+
(accessor, locations, question) => engine.ask(accessor, locations, question),
|
|
181
|
+
);
|
|
146
182
|
// The snapshot is never updated, so a project's own file set is the same answer
|
|
147
183
|
// every time it is asked for. It is read once because the question is not free:
|
|
148
184
|
// deciding it consults the metadata of every file the program holds, the compiler's
|
|
149
185
|
// own library files included, and a run asks for the set once per pass.
|
|
150
186
|
let ownFiles: readonly SourceFile[] | undefined;
|
|
187
|
+
let ownKeys: ReadonlySet<string> | undefined;
|
|
188
|
+
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);
|
|
203
|
+
return ownFiles;
|
|
204
|
+
};
|
|
151
205
|
return {
|
|
152
|
-
configFile
|
|
206
|
+
configFile,
|
|
153
207
|
program: project.program,
|
|
154
|
-
|
|
155
|
-
ownSourceFiles
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
// name. Fetching each file first and filtering afterwards costs one round
|
|
160
|
-
// trip per library file the answer then discards, and a program holds far
|
|
161
|
-
// more library files than target files.
|
|
162
|
-
.filter((name) => {
|
|
163
|
-
const metadata = project.program.getSourceFileMetadata(name);
|
|
164
|
-
return (
|
|
165
|
-
metadata !== undefined && !metadata.isDefaultLibrary && !metadata.isFromExternalLibrary
|
|
166
|
-
);
|
|
167
|
-
})
|
|
168
|
-
.map((name) => project.program.getSourceFile(name))
|
|
169
|
-
.filter((file): file is SourceFile => file !== undefined);
|
|
170
|
-
return ownFiles;
|
|
208
|
+
queries,
|
|
209
|
+
ownSourceFiles,
|
|
210
|
+
ownPaths: () => {
|
|
211
|
+
ownKeys ??= new Set(ownSourceFiles().map((file) => file.path));
|
|
212
|
+
return ownKeys;
|
|
171
213
|
},
|
|
172
214
|
handle: (node) => ({ node }),
|
|
173
|
-
symbolsAt: (handles
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
215
|
+
symbolsAt: (handles, cap) =>
|
|
216
|
+
queries.symbolsAt(
|
|
217
|
+
handles.map(({ node }) => node),
|
|
218
|
+
cap,
|
|
219
|
+
),
|
|
220
|
+
resolvedSymbolAt: ({ node }) => queries.resolvedSymbol(node),
|
|
221
|
+
shorthandValueAt: ({ node }) => queries.shorthandValue(node),
|
|
177
222
|
aliasStepOf: (symbol) => {
|
|
178
223
|
// The step is asked of an alias only: asked of any other symbol, the compiler
|
|
179
224
|
// fails an assertion and the session ends.
|
|
180
225
|
if ((symbol.flags & SymbolFlags.Alias) === 0) {
|
|
181
226
|
return undefined;
|
|
182
227
|
}
|
|
183
|
-
const held =
|
|
228
|
+
const held = queries.immediateAliased(symbol);
|
|
184
229
|
// An alias that resolves to nothing answers with the checker's own unknown
|
|
185
230
|
// symbol, which declares nothing; a symbol that is no alias answers with
|
|
186
231
|
// nothing at all.
|
|
187
|
-
return held ===
|
|
232
|
+
return typeof held === "object" && held.declarations.length === 0 ? undefined : held;
|
|
188
233
|
},
|
|
189
234
|
declarationAt: (handle) => {
|
|
190
235
|
const node = handle.resolve(project);
|
|
@@ -203,11 +248,17 @@ export function narrowedTo<Brand>(
|
|
|
203
248
|
keep: (file: SourceFile) => boolean,
|
|
204
249
|
): ProjectView<Brand> {
|
|
205
250
|
let kept: readonly SourceFile[] | undefined;
|
|
251
|
+
let keys: ReadonlySet<string> | undefined;
|
|
252
|
+
const ownSourceFiles = (): readonly SourceFile[] => {
|
|
253
|
+
kept ??= project.ownSourceFiles().filter(keep);
|
|
254
|
+
return kept;
|
|
255
|
+
};
|
|
206
256
|
return {
|
|
207
257
|
...project,
|
|
208
|
-
ownSourceFiles
|
|
209
|
-
|
|
210
|
-
|
|
258
|
+
ownSourceFiles,
|
|
259
|
+
ownPaths: () => {
|
|
260
|
+
keys ??= new Set(ownSourceFiles().map((file) => file.path));
|
|
261
|
+
return keys;
|
|
211
262
|
},
|
|
212
263
|
};
|
|
213
264
|
}
|
|
@@ -218,6 +269,11 @@ export interface ProjectDiagnostics {
|
|
|
218
269
|
readonly syntactic: readonly Diagnostic[];
|
|
219
270
|
readonly semantic: readonly Diagnostic[];
|
|
220
271
|
readonly configParsing: readonly Diagnostic[];
|
|
272
|
+
/** The line and column of one offset into one of the program's files, counted from one. */
|
|
273
|
+
readonly locate: (
|
|
274
|
+
fileName: string,
|
|
275
|
+
offset: number,
|
|
276
|
+
) => { line: number; column: number } | undefined;
|
|
221
277
|
}
|
|
222
278
|
|
|
223
279
|
/** The three diagnostic sets one project carries, read before any analysis. */
|
|
@@ -227,15 +283,31 @@ export function diagnosticsOf<Brand>(project: ProjectView<Brand>): ProjectDiagno
|
|
|
227
283
|
syntactic: project.program.getSyntacticDiagnostics(),
|
|
228
284
|
semantic: project.program.getSemanticDiagnostics(),
|
|
229
285
|
configParsing: project.program.getConfigFileParsingDiagnostics(),
|
|
286
|
+
locate: (fileName, offset) => {
|
|
287
|
+
const file = project.program.getSourceFile(fileName);
|
|
288
|
+
if (file === undefined || offset < 0 || offset > file.text.length) {
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
const { line, character } = file.getLineAndCharacterOfPosition(offset);
|
|
292
|
+
return { line: line + 1, column: character + 1 };
|
|
293
|
+
},
|
|
230
294
|
};
|
|
231
295
|
}
|
|
232
296
|
|
|
233
297
|
/** One run's result: what the visitor answered for each project, and what it cost. */
|
|
234
298
|
export interface SessionResult<Result> {
|
|
299
|
+
/** One answer per configuration the snapshot opened a project for, in the order named. */
|
|
235
300
|
readonly projects: readonly Result[];
|
|
301
|
+
/** The configurations the snapshot opened no project for, in the order named. */
|
|
302
|
+
readonly unopened: readonly string[];
|
|
236
303
|
readonly timing: TimingInfo;
|
|
304
|
+
/** Every question the checker could not answer, each once, in the order first asked. */
|
|
305
|
+
readonly unanswered: readonly UnansweredQuestion[];
|
|
237
306
|
}
|
|
238
307
|
|
|
308
|
+
/** What a run does about a configuration the snapshot opened no project for. */
|
|
309
|
+
type Unopened = "refuse" | "record";
|
|
310
|
+
|
|
239
311
|
/**
|
|
240
312
|
* Runs `visit` over every project the compiler configurations name, inside one
|
|
241
313
|
* snapshot of one client, in the order they are named, and answers in that order.
|
|
@@ -244,30 +316,46 @@ export interface SessionResult<Result> {
|
|
|
244
316
|
* with its own program and checker; it is never updated, because the analysis
|
|
245
317
|
* reads and the tree does not change under it. The order the snapshot lists its
|
|
246
318
|
* projects in is the compiler's own, so each named configuration is looked up in it
|
|
247
|
-
* instead
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
319
|
+
* instead. A configuration the snapshot opened no project for ends the run under
|
|
320
|
+
* `"refuse"` and is named in the result under `"record"`. The snapshot is disposed once
|
|
321
|
+
* the last project has been visited, and the client is released after it, whether the
|
|
322
|
+
* visit completed or threw.
|
|
251
323
|
*/
|
|
252
324
|
export function runSession<Result>(
|
|
253
325
|
engine: Engine,
|
|
254
326
|
configFiles: readonly string[],
|
|
255
327
|
visit: ProjectVisitor<Result>,
|
|
328
|
+
unopened: Unopened = "refuse",
|
|
256
329
|
): SessionResult<Result> {
|
|
257
330
|
try {
|
|
258
|
-
const snapshot = engine.
|
|
331
|
+
const snapshot = engine.createSnapshot(configFiles);
|
|
259
332
|
try {
|
|
260
|
-
const opened = new Map(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
333
|
+
const opened = new Map<string, Project>();
|
|
334
|
+
for (const project of snapshot.getProjects()) {
|
|
335
|
+
if (project.configFileName !== undefined) {
|
|
336
|
+
opened.set(project.configFileName, project);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const log = new UnansweredLog();
|
|
340
|
+
const projects: Result[] = [];
|
|
341
|
+
const missing: string[] = [];
|
|
342
|
+
for (const configFile of configFiles) {
|
|
264
343
|
const project = opened.get(configFile);
|
|
265
344
|
if (project === undefined) {
|
|
266
|
-
|
|
345
|
+
if (unopened === "refuse") {
|
|
346
|
+
throw new DiscoveryError(`${configFile} opened no project`, []);
|
|
347
|
+
}
|
|
348
|
+
missing.push(configFile);
|
|
349
|
+
continue;
|
|
267
350
|
}
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
return {
|
|
351
|
+
projects.push(visit(viewOf(project, configFile, engine, log)));
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
projects,
|
|
355
|
+
unopened: missing,
|
|
356
|
+
timing: engine.getTimingInfo(),
|
|
357
|
+
unanswered: log.questions,
|
|
358
|
+
};
|
|
271
359
|
} finally {
|
|
272
360
|
snapshot.dispose();
|
|
273
361
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The function a declaration of the inventory declares, where it declares one: the node
|
|
3
|
+
* whose parameters, body and result are the declaration's own parts.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
isArrowFunction,
|
|
8
|
+
isClassDeclaration,
|
|
9
|
+
isClassExpression,
|
|
10
|
+
isFunctionDeclaration,
|
|
11
|
+
isFunctionExpression,
|
|
12
|
+
isMethodDeclaration,
|
|
13
|
+
isPropertyDeclaration,
|
|
14
|
+
isVariableDeclaration,
|
|
15
|
+
type ArrowFunction,
|
|
16
|
+
type FunctionDeclaration,
|
|
17
|
+
type FunctionExpression,
|
|
18
|
+
type MethodDeclaration,
|
|
19
|
+
type Node,
|
|
20
|
+
} from "@typescript/native/unstable/ast";
|
|
21
|
+
|
|
22
|
+
/** A function-like node that carries a body of its own. */
|
|
23
|
+
export type SignatureNode =
|
|
24
|
+
FunctionDeclaration | MethodDeclaration | ArrowFunction | FunctionExpression;
|
|
25
|
+
|
|
26
|
+
/** Whether one node is a function value written in place: an arrow or a function expression. */
|
|
27
|
+
function isFunctionValue(node: Node | undefined): node is ArrowFunction | FunctionExpression {
|
|
28
|
+
return node !== undefined && (isArrowFunction(node) || isFunctionExpression(node));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The function one declaration declares: a function declaration and a class method with
|
|
33
|
+
* a body are their own; a variable and a class property whose initializer is an arrow or
|
|
34
|
+
* a function expression declare that function. Any other declaration declares none, an
|
|
35
|
+
* overload signature and an abstract or ambient method among them, because no body is
|
|
36
|
+
* there to read its parts.
|
|
37
|
+
*/
|
|
38
|
+
export function signatureOf(node: Node): SignatureNode | undefined {
|
|
39
|
+
if (isFunctionDeclaration(node)) {
|
|
40
|
+
return node.body === undefined ? undefined : node;
|
|
41
|
+
}
|
|
42
|
+
if (isMethodDeclaration(node)) {
|
|
43
|
+
const owner = node.parent;
|
|
44
|
+
return node.body !== undefined && (isClassDeclaration(owner) || isClassExpression(owner))
|
|
45
|
+
? node
|
|
46
|
+
: undefined;
|
|
47
|
+
}
|
|
48
|
+
if (
|
|
49
|
+
(isVariableDeclaration(node) || isPropertyDeclaration(node)) &&
|
|
50
|
+
isFunctionValue(node.initializer)
|
|
51
|
+
) {
|
|
52
|
+
return node.initializer;
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
package/src/suppress-file.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { Host } from "./host.ts";
|
|
9
9
|
import type { InventorySymbol } from "./inventory.ts";
|
|
10
|
+
import { jsonChunks } from "./json-chunks.ts";
|
|
10
11
|
import { joinPath } from "./paths.ts";
|
|
11
12
|
import { positionAt, walkDocument } from "./json-document.ts";
|
|
12
13
|
import type { Position } from "./position.ts";
|
|
@@ -154,6 +155,15 @@ function readRecord(value: unknown, site: Position, byReference: ByReference, sh
|
|
|
154
155
|
return { records: named.map((one) => ({ ...written, bound: one.id })), refusals: [] };
|
|
155
156
|
}
|
|
156
157
|
|
|
158
|
+
function byReferenceOf(symbols: readonly InventorySymbol[]): ByReference {
|
|
159
|
+
const byReference = new Map<string, InventorySymbol[]>();
|
|
160
|
+
for (const symbol of symbols) {
|
|
161
|
+
const key = referenceKey(symbol.ref, symbol.position.path);
|
|
162
|
+
byReference.set(key, [...(byReference.get(key) ?? []), symbol]);
|
|
163
|
+
}
|
|
164
|
+
return byReference;
|
|
165
|
+
}
|
|
166
|
+
|
|
157
167
|
/** Reads one document at the target root, binding each record against the run's declarations. */
|
|
158
168
|
function readDocument(
|
|
159
169
|
host: Host,
|
|
@@ -196,11 +206,7 @@ function readDocument(
|
|
|
196
206
|
return malformed(shape.file);
|
|
197
207
|
}
|
|
198
208
|
|
|
199
|
-
const byReference =
|
|
200
|
-
for (const symbol of symbols) {
|
|
201
|
-
const key = referenceKey(symbol.ref, symbol.position.path);
|
|
202
|
-
byReference.set(key, [...(byReference.get(key) ?? []), symbol]);
|
|
203
|
-
}
|
|
209
|
+
const byReference = byReferenceOf(symbols);
|
|
204
210
|
const records: SuppressionRecord[] = [];
|
|
205
211
|
const refusals: Refusal[] = [];
|
|
206
212
|
(held as readonly unknown[]).forEach((value, index) => {
|
|
@@ -247,6 +253,24 @@ export interface Recorded {
|
|
|
247
253
|
readonly path: string;
|
|
248
254
|
}
|
|
249
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Rows a baseline write holds in memory, bound against the run's declarations as the same
|
|
258
|
+
* rows read back from the document would be, each carrying the reason given.
|
|
259
|
+
*/
|
|
260
|
+
export function bindRows(
|
|
261
|
+
rows: readonly Recorded[],
|
|
262
|
+
reason: string,
|
|
263
|
+
symbols: readonly InventorySymbol[],
|
|
264
|
+
): Suppressions {
|
|
265
|
+
const byReference = byReferenceOf(symbols);
|
|
266
|
+
const site = documentSite(BASELINE_SHAPE);
|
|
267
|
+
const read = rows.map((row) => readRecord({ ...row, reason }, site, byReference, BASELINE_SHAPE));
|
|
268
|
+
return {
|
|
269
|
+
records: read.flatMap((one) => one.records),
|
|
270
|
+
refusals: read.flatMap((one) => one.refusals),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
250
274
|
/** The analyzer a written row names as its provenance, as the same run's report names it. */
|
|
251
275
|
export interface Provenance {
|
|
252
276
|
readonly analyzer: string;
|
|
@@ -258,19 +282,27 @@ const BASELINE_DESCRIPTION =
|
|
|
258
282
|
"Recorded findings: a run fails only on a finding this document does not hold. " +
|
|
259
283
|
"Every reason is the analyzer that recorded the row, never an adjudication.";
|
|
260
284
|
|
|
285
|
+
/** The reason every row a baseline write records carries. */
|
|
286
|
+
export function rowReason(provenance: Provenance): string {
|
|
287
|
+
return `recorded by ${provenance.analyzer} ${provenance.version}`;
|
|
288
|
+
}
|
|
289
|
+
|
|
261
290
|
/**
|
|
262
291
|
* A baseline holding one row per finding, in the order given, which is the report's. Every
|
|
263
292
|
* row carries the provenance as its reason. A finding missing a value a row needs, and an
|
|
264
293
|
* identity missing its name or version, are refused rather than written, because the
|
|
265
294
|
* grammar requires a reason on every row.
|
|
266
295
|
*/
|
|
267
|
-
export function writeBaseline(
|
|
296
|
+
export function writeBaseline(
|
|
297
|
+
findings: readonly Recorded[],
|
|
298
|
+
provenance: Provenance,
|
|
299
|
+
): Iterable<string> {
|
|
268
300
|
if (provenance.analyzer === "" || provenance.version === "") {
|
|
269
301
|
throw new Error(
|
|
270
302
|
`a baseline row would carry no reason: the analyzer identity names ${JSON.stringify(provenance.analyzer)} at version ${JSON.stringify(provenance.version)}`,
|
|
271
303
|
);
|
|
272
304
|
}
|
|
273
|
-
const reason =
|
|
305
|
+
const reason = rowReason(provenance);
|
|
274
306
|
const baseline = findings.map((found) => {
|
|
275
307
|
if (found.code === "" || found.symbol === "" || found.path === "") {
|
|
276
308
|
throw new Error(
|
|
@@ -279,5 +311,5 @@ export function writeBaseline(findings: readonly Recorded[], provenance: Provena
|
|
|
279
311
|
}
|
|
280
312
|
return { code: found.code, symbol: found.symbol, path: found.path, reason };
|
|
281
313
|
});
|
|
282
|
-
return
|
|
314
|
+
return jsonChunks({ description: BASELINE_DESCRIPTION, baseline }, 2);
|
|
283
315
|
}
|
package/src/suppress-inline.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { SyntaxKind, type Node, type SourceFile } from "@typescript/native/unsta
|
|
|
8
8
|
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
9
9
|
import { byPosition, positionKey, renderPosition, type Position } from "./position.ts";
|
|
10
10
|
import type { ProjectView } from "./session.ts";
|
|
11
|
+
import { signatureOf } from "./signatures.ts";
|
|
11
12
|
import {
|
|
12
13
|
SUPPRESSION_WITHOUT_REASON,
|
|
13
14
|
SuppressionError,
|
|
@@ -168,7 +169,8 @@ interface Declared {
|
|
|
168
169
|
/**
|
|
169
170
|
* The declarations of one file by the line each begins on, the line of its first token: a
|
|
170
171
|
* decorator, a modifier or the keyword. A variable's first token is its statement's
|
|
171
|
-
* keyword, and a variable written on a later line of the statement also begins there.
|
|
172
|
+
* keyword, and a variable written on a later line of the statement also begins there. A
|
|
173
|
+
* function also begins, for a directive, on the line of each of its parameters.
|
|
172
174
|
*/
|
|
173
175
|
function declarationLines(
|
|
174
176
|
file: SourceFile,
|
|
@@ -189,6 +191,11 @@ function declarationLines(
|
|
|
189
191
|
const one = id === undefined ? undefined : symbols.get(id);
|
|
190
192
|
if (one !== undefined) {
|
|
191
193
|
add(node.pos, one);
|
|
194
|
+
// A parameter on a later line of a wrapped signature takes its directive on the
|
|
195
|
+
// line above it, inside the parameter list, and the directive binds the function.
|
|
196
|
+
for (const parameter of signatureOf(node)?.parameters ?? []) {
|
|
197
|
+
add(parameter.pos, one);
|
|
198
|
+
}
|
|
192
199
|
const list = node.parent;
|
|
193
200
|
if (
|
|
194
201
|
node.kind === SyntaxKind.VariableDeclaration &&
|
package/src/suppress.ts
CHANGED
|
@@ -169,6 +169,7 @@ const ROW_SUBJECTS: ReadonlySet<string> = new Set([
|
|
|
169
169
|
"dependency",
|
|
170
170
|
"module-directive",
|
|
171
171
|
"root",
|
|
172
|
+
"configured-declaration",
|
|
172
173
|
"suppression",
|
|
173
174
|
"edge",
|
|
174
175
|
]);
|
|
@@ -178,6 +179,36 @@ export function isRowSubject(kind: string): boolean {
|
|
|
178
179
|
return ROW_SUBJECTS.has(kind);
|
|
179
180
|
}
|
|
180
181
|
|
|
182
|
+
/**
|
|
183
|
+
* The subject kinds that are a part of a declaration rather than a declaration: a part
|
|
184
|
+
* has no reference of its own, so a finding about one names the declaration that holds
|
|
185
|
+
* it, and the part is decided inside that declaration whatever uses it.
|
|
186
|
+
*/
|
|
187
|
+
const PART_SUBJECTS: ReadonlySet<string> = new Set([
|
|
188
|
+
"parameter",
|
|
189
|
+
"receiver",
|
|
190
|
+
"result",
|
|
191
|
+
"statement",
|
|
192
|
+
"case",
|
|
193
|
+
"store",
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
/** Whether a finding's subject is a part of the declaration its reference names. */
|
|
197
|
+
export function isPartSubject(kind: string): boolean {
|
|
198
|
+
return PART_SUBJECTS.has(kind);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The prefix of the codes of the part kinds, the intra-function range. */
|
|
202
|
+
const PART_KIND_PREFIX = "DS18";
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Whether a code is a part kind, whose record withholds its finding and marks nothing:
|
|
206
|
+
* the record says the part is wanted, not that the declaration holding it is.
|
|
207
|
+
*/
|
|
208
|
+
export function isPartKind(code: string): boolean {
|
|
209
|
+
return code.startsWith(PART_KIND_PREFIX);
|
|
210
|
+
}
|
|
211
|
+
|
|
181
212
|
/** One finding's identity: its code and the position of the thing it names. */
|
|
182
213
|
function claimKey(finding: Finding): string {
|
|
183
214
|
return `${finding.code}\u0000${positionKey(finding.position)}`;
|
package/src/sweep.ts
CHANGED
|
@@ -51,6 +51,11 @@ export interface SweepInput {
|
|
|
51
51
|
* holds nothing back.
|
|
52
52
|
*/
|
|
53
53
|
readonly exempt?: readonly Exemption[];
|
|
54
|
+
/**
|
|
55
|
+
* The declarations a question the checker did not answer could have kept live. Each
|
|
56
|
+
* is held as an exempt declaration is. Absent, the sweep holds nothing back for one.
|
|
57
|
+
*/
|
|
58
|
+
readonly unanswered?: readonly string[];
|
|
54
59
|
readonly mode: Mode;
|
|
55
60
|
}
|
|
56
61
|
|
|
@@ -94,10 +99,10 @@ export interface Liveness {
|
|
|
94
99
|
*/
|
|
95
100
|
export function sweep(graph: Graph, input: SweepInput): Liveness {
|
|
96
101
|
const marked = flagged(graph, input.marked);
|
|
97
|
-
const exempt = flagged(
|
|
98
|
-
|
|
99
|
-
(input.
|
|
100
|
-
);
|
|
102
|
+
const exempt = flagged(graph, [
|
|
103
|
+
...(input.exempt ?? []).map((record) => record.id),
|
|
104
|
+
...(input.unanswered ?? []),
|
|
105
|
+
]);
|
|
101
106
|
const held = marked.map((mark, at) => mark || exempt[at] === true);
|
|
102
107
|
// Every root, every mark and every exemption seeds reachability. A root of a kind that
|
|
103
108
|
// names a caller, and a mark, are live under reference counting as well; a root that
|