@cplieger/deadset-ts 3.13.0 → 4.0.1
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 +67 -49
- package/dist/bin/node-host.js +10 -7
- package/dist/src/alias-chain.js +59 -7
- package/dist/src/analysis.js +120 -24
- package/dist/src/calls.js +23 -22
- package/dist/src/class-members.js +10 -11
- package/dist/src/configured-declarations.js +22 -9
- 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 +106 -29
- package/dist/src/entry-point-gaps.js +2 -2
- 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/emitters.js +15 -1
- package/dist/src/findings/interfaces.js +7 -1
- 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 +37 -20
- package/dist/src/inventory.js +85 -4
- package/dist/src/json-chunks.js +76 -0
- 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 +18 -9
- package/dist/src/report.js +3 -1
- package/dist/src/reporters/annotations.js +9 -5
- package/dist/src/reporters/json.js +3 -2
- package/dist/src/reporters/sarif.js +2 -1
- package/dist/src/reporters/template.js +51 -10
- package/dist/src/reporters/text.js +8 -6
- package/dist/src/roots.js +27 -11
- package/dist/src/schema.js +2 -2
- package/dist/src/serialization-contract.js +12 -10
- package/dist/src/session.js +79 -39
- package/dist/src/suppress-file.js +2 -1
- package/dist/src/sweep.js +4 -1
- package/dist/src/value-reach.js +14 -19
- package/dist/src/verbs/analyze.js +20 -3
- package/dist/src/verbs/explain.js +22 -9
- package/dist/src/verbs/print-projects.js +41 -8
- package/dist/src/verbs/print-retained.js +6 -2
- package/dist/src/verbs/print-roots.js +5 -1
- package/jsr.json +1 -1
- package/package.json +3 -3
- package/src/alias-chain.ts +75 -11
- package/src/analysis.ts +224 -62
- package/src/calls.ts +10 -9
- package/src/class-members.ts +10 -11
- package/src/configured-declarations.ts +29 -13
- package/src/dependencies.ts +42 -20
- package/src/deprecation.ts +53 -27
- package/src/destructuring.ts +50 -39
- package/src/discover.ts +178 -34
- package/src/entry-point-gaps.ts +2 -2
- package/src/entry-points.ts +7 -0
- package/src/enum-group.ts +56 -42
- package/src/exempt.ts +50 -2
- package/src/findings/emitters.ts +18 -1
- package/src/findings/interfaces.ts +7 -1
- 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 +43 -25
- package/src/inventory.ts +97 -4
- package/src/json-chunks.ts +87 -0
- package/src/partial.ts +40 -0
- package/src/query.ts +522 -0
- package/src/references.ts +119 -38
- package/src/reflective-lookup.ts +21 -11
- package/src/report.ts +14 -2
- 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 +3 -2
- package/src/reporters/template.ts +53 -11
- package/src/reporters/text.ts +12 -12
- package/src/roots.ts +34 -12
- package/src/schema.ts +2 -2
- package/src/serialization-contract.ts +12 -10
- package/src/session.ts +146 -58
- package/src/suppress-file.ts +6 -2
- package/src/sweep.ts +9 -4
- package/src/value-reach.ts +20 -23
- package/src/verbs/analyze.ts +25 -3
- package/src/verbs/explain.ts +20 -9
- package/src/verbs/print-projects.ts +54 -8
- package/src/verbs/print-retained.ts +6 -2
- package/src/verbs/print-roots.ts +5 -1
package/src/discover.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { BuildConfiguration, ProjectConfiguration } from "./config.ts";
|
|
|
4
4
|
import type { Host } from "./host.ts";
|
|
5
5
|
import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.ts";
|
|
6
6
|
import type { Scope } from "./scope.ts";
|
|
7
|
-
import type { Engine } from "./session.ts";
|
|
7
|
+
import type { Engine, ProjectDiagnostics } from "./session.ts";
|
|
8
8
|
|
|
9
9
|
/** Directory names discovery does not descend into. */
|
|
10
10
|
export const IGNORED_DIRECTORIES: ReadonlySet<string> = new Set(["node_modules", ".git"]);
|
|
@@ -20,24 +20,60 @@ const CONFIG_FILE = /^tsconfig.*\.json$/u;
|
|
|
20
20
|
*/
|
|
21
21
|
export class DiscoveryError extends Error {
|
|
22
22
|
/** The diagnostics the compiler reported, in the order it reported them. */
|
|
23
|
-
readonly diagnostics: readonly
|
|
23
|
+
readonly diagnostics: readonly LocatedDiagnostic[];
|
|
24
24
|
|
|
25
|
-
constructor(message: string, diagnostics: readonly
|
|
25
|
+
constructor(message: string, diagnostics: readonly LocatedDiagnostic[]) {
|
|
26
26
|
super(message);
|
|
27
27
|
this.name = "DiscoveryError";
|
|
28
28
|
this.diagnostics = diagnostics;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/** The category an error diagnostic carries, which the protocol sends as a number. */
|
|
33
|
+
const ERROR_CATEGORY: number = DiagnosticCategory.Error;
|
|
34
|
+
|
|
32
35
|
/** Whether one diagnostic is an error, which is what fails a run closed. */
|
|
33
36
|
export function isError(diagnostic: Diagnostic): boolean {
|
|
34
|
-
return diagnostic.category ===
|
|
37
|
+
return diagnostic.category === ERROR_CATEGORY;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
/** One diagnostic
|
|
38
|
-
export
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
/** One diagnostic, with the line and column its offset falls on where it names a file. */
|
|
41
|
+
export interface LocatedDiagnostic {
|
|
42
|
+
readonly diagnostic: Diagnostic;
|
|
43
|
+
/** Where the diagnostic starts, counted from one; absent where it names no position. */
|
|
44
|
+
readonly at?: { readonly line: number; readonly column: number } | undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* One diagnostic as a line naming its file, its line and column, its code and its
|
|
49
|
+
* message: `path:line:column: TScode: message`, the position left out where the
|
|
50
|
+
* diagnostic names none and the file where it names none. `display` spells the
|
|
51
|
+
* file's path; it answers the path itself where it is not given.
|
|
52
|
+
*/
|
|
53
|
+
export function renderDiagnostic(
|
|
54
|
+
located: LocatedDiagnostic,
|
|
55
|
+
display: (path: string) => string = (path) => path,
|
|
56
|
+
): string {
|
|
57
|
+
const { diagnostic, at } = located;
|
|
58
|
+
const message = `TS${String(diagnostic.code)}: ${diagnostic.text}`;
|
|
59
|
+
if (diagnostic.fileName === undefined) {
|
|
60
|
+
return message;
|
|
61
|
+
}
|
|
62
|
+
const where = display(diagnostic.fileName);
|
|
63
|
+
return at === undefined
|
|
64
|
+
? `${where}: ${message}`
|
|
65
|
+
: `${where}:${String(at.line)}:${String(at.column)}: ${message}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Each diagnostic with the line and column its offset falls on. */
|
|
69
|
+
function locatedIn(
|
|
70
|
+
diagnostics: readonly Diagnostic[],
|
|
71
|
+
locate: (fileName: string, offset: number) => { line: number; column: number } | undefined,
|
|
72
|
+
): readonly LocatedDiagnostic[] {
|
|
73
|
+
return diagnostics.map((diagnostic) => ({
|
|
74
|
+
diagnostic,
|
|
75
|
+
at: diagnostic.fileName === undefined ? undefined : locate(diagnostic.fileName, diagnostic.pos),
|
|
76
|
+
}));
|
|
41
77
|
}
|
|
42
78
|
|
|
43
79
|
/**
|
|
@@ -55,6 +91,26 @@ export function diagnosticErrors(sets: {
|
|
|
55
91
|
return [...sets.configParsing, ...sets.syntactic, ...sets.semantic].filter(isError);
|
|
56
92
|
}
|
|
57
93
|
|
|
94
|
+
/** Why one open project cannot be analyzed, split by whether it loaded at all. */
|
|
95
|
+
interface ProjectErrors {
|
|
96
|
+
/**
|
|
97
|
+
* The errors in the configuration itself: no input matched, an option that does not
|
|
98
|
+
* parse. A project carrying one did not load, whatever its files say.
|
|
99
|
+
*/
|
|
100
|
+
readonly load: readonly LocatedDiagnostic[];
|
|
101
|
+
/** The errors in the files of a project that loaded: their syntax and their types. */
|
|
102
|
+
readonly check: readonly LocatedDiagnostic[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The errors one open project carries, each located in the file it names. */
|
|
106
|
+
export function projectErrors(sets: ProjectDiagnostics): ProjectErrors {
|
|
107
|
+
const load = sets.configParsing.filter(isError);
|
|
108
|
+
return {
|
|
109
|
+
load: locatedIn(load, sets.locate),
|
|
110
|
+
check: load.length > 0 ? [] : locatedIn(diagnosticErrors(sets), sets.locate),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
58
114
|
/** One project a run analyzes. */
|
|
59
115
|
export interface DiscoveredProject {
|
|
60
116
|
/**
|
|
@@ -68,10 +124,54 @@ export interface DiscoveredProject {
|
|
|
68
124
|
readonly configFile: string;
|
|
69
125
|
}
|
|
70
126
|
|
|
127
|
+
/**
|
|
128
|
+
* One project discovery derived and the run could not build, with the first line of
|
|
129
|
+
* the error that dropped it.
|
|
130
|
+
*/
|
|
131
|
+
export interface NotBuilt {
|
|
132
|
+
/** The name the project would have carried, as {@link DiscoveredProject.id} spells it. */
|
|
133
|
+
readonly id: string;
|
|
134
|
+
/** The compiler configuration file, absolute. */
|
|
135
|
+
readonly configFile: string;
|
|
136
|
+
/**
|
|
137
|
+
* The error's first line, every path below the target root spelled relative to it, so
|
|
138
|
+
* a report naming it carries no path of the machine it ran on.
|
|
139
|
+
*/
|
|
140
|
+
readonly error: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The first line of one error about the target, every mention of the target root
|
|
145
|
+
* spelled relative to it: a path below the root loses the root and its separator, and
|
|
146
|
+
* the root itself is `.`.
|
|
147
|
+
*/
|
|
148
|
+
export function targetRelative(text: string, targetRoot: string): string {
|
|
149
|
+
const root = targetRoot.endsWith("/") ? targetRoot.slice(0, -1) : targetRoot;
|
|
150
|
+
const [first = ""] = text.split(/\r?\n/u);
|
|
151
|
+
const spelled = first.split(`${root}/`).join("").split(root).join(".").trim();
|
|
152
|
+
return spelled === "" ? "the configuration could not be built" : spelled;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Whether one configuration is the analyzer's own guess: discovery derived it and the
|
|
157
|
+
* scope did not name it. A guess that does not load is dropped; any other ends the run.
|
|
158
|
+
*/
|
|
159
|
+
export function isGuess(discovery: Discovery, configFile: string): boolean {
|
|
160
|
+
return discovery.derived && !discovery.fromScope.includes(configFile);
|
|
161
|
+
}
|
|
162
|
+
|
|
71
163
|
/** Every project discovery found, and where each came from. */
|
|
72
164
|
export interface Discovery {
|
|
73
165
|
/** The projects, in discovery order, each once. */
|
|
74
166
|
readonly projects: readonly DiscoveredProject[];
|
|
167
|
+
/**
|
|
168
|
+
* Whether discovery derived the projects rather than reading them from a declared
|
|
169
|
+
* matrix. A derived project that fails to load is dropped and named in
|
|
170
|
+
* {@link Discovery.notBuilt}; a declared one ends the run.
|
|
171
|
+
*/
|
|
172
|
+
readonly derived: boolean;
|
|
173
|
+
/** The derived configurations the compiler could not read, in discovery order. */
|
|
174
|
+
readonly notBuilt: readonly NotBuilt[];
|
|
75
175
|
/** The configuration file of each project, in the same order. */
|
|
76
176
|
readonly configFiles: readonly string[];
|
|
77
177
|
/**
|
|
@@ -174,9 +274,16 @@ function scopeConfigFiles(host: Host, scope: Scope): string[] {
|
|
|
174
274
|
/**
|
|
175
275
|
* Every configuration file reached from `pending`, in the order discovery reads them:
|
|
176
276
|
* each once, each followed by the configurations it references, transitively. A
|
|
177
|
-
* configuration the compiler cannot read ends the run naming it
|
|
277
|
+
* configuration the compiler cannot read ends the run naming it, unless `unreadable`
|
|
278
|
+
* is given, which is then told the configuration and why, and the walk goes on
|
|
279
|
+
* without it.
|
|
178
280
|
*/
|
|
179
|
-
function derivedFrom(
|
|
281
|
+
function derivedFrom(
|
|
282
|
+
engine: Engine,
|
|
283
|
+
host: Host,
|
|
284
|
+
pending: string[],
|
|
285
|
+
unreadable?: (configFile: string, reason: string) => void,
|
|
286
|
+
): string[] {
|
|
180
287
|
const seen = new Set<string>();
|
|
181
288
|
const found: string[] = [];
|
|
182
289
|
while (pending.length > 0) {
|
|
@@ -189,7 +296,17 @@ function derivedFrom(engine: Engine, host: Host, pending: string[]): string[] {
|
|
|
189
296
|
continue;
|
|
190
297
|
}
|
|
191
298
|
seen.add(path);
|
|
192
|
-
|
|
299
|
+
const reason = unreadableReason(engine, path);
|
|
300
|
+
if (reason !== undefined) {
|
|
301
|
+
if (unreadable === undefined) {
|
|
302
|
+
throw new DiscoveryError(
|
|
303
|
+
`${path} cannot be read as a compiler configuration: ${reason}`,
|
|
304
|
+
[],
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
unreadable(path, reason);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
193
310
|
found.push(path);
|
|
194
311
|
pending.push(...referencesOf(host, path));
|
|
195
312
|
}
|
|
@@ -232,20 +349,25 @@ function derivedId(targetRoot: string, configFile: string): string {
|
|
|
232
349
|
return relativePath(targetRoot, configFile) ?? configFile;
|
|
233
350
|
}
|
|
234
351
|
|
|
352
|
+
/** Why the compiler cannot read one configuration file, or undefined where it can. */
|
|
353
|
+
function unreadableReason(engine: Engine, configFile: string): string | undefined {
|
|
354
|
+
try {
|
|
355
|
+
engine.parseConfigFile(configFile);
|
|
356
|
+
return undefined;
|
|
357
|
+
} catch (error: unknown) {
|
|
358
|
+
return error instanceof Error ? error.message : String(error);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
235
362
|
/**
|
|
236
363
|
* Resolves one compiler configuration file, or ends the run naming it: a project
|
|
237
364
|
* opened from a configuration the compiler cannot read would be analyzed with no
|
|
238
365
|
* type information.
|
|
239
366
|
*/
|
|
240
367
|
function parseOrRefuse(engine: Engine, configFile: string, named: string): void {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
throw new DiscoveryError(
|
|
245
|
-
`${named} cannot be read as a compiler configuration: ` +
|
|
246
|
-
(error instanceof Error ? error.message : String(error)),
|
|
247
|
-
[],
|
|
248
|
-
);
|
|
368
|
+
const reason = unreadableReason(engine, configFile);
|
|
369
|
+
if (reason !== undefined) {
|
|
370
|
+
throw new DiscoveryError(`${named} cannot be read as a compiler configuration: ${reason}`, []);
|
|
249
371
|
}
|
|
250
372
|
}
|
|
251
373
|
|
|
@@ -293,32 +415,56 @@ function declaredProjects(
|
|
|
293
415
|
if (unnamed !== undefined) {
|
|
294
416
|
throw new DiscoveryError(
|
|
295
417
|
`the project ${JSON.stringify(project.id)} (${project.configFile}) references ` +
|
|
296
|
-
`${unnamed}, which the build matrix does not name
|
|
418
|
+
`${unnamed}, which the build matrix does not name. A referenced project is built ` +
|
|
297
419
|
"into the program that references it, so the matrix names it too",
|
|
298
420
|
[],
|
|
299
421
|
);
|
|
300
422
|
}
|
|
301
423
|
}
|
|
302
|
-
return {
|
|
424
|
+
return {
|
|
425
|
+
projects,
|
|
426
|
+
derived: false,
|
|
427
|
+
notBuilt: [],
|
|
428
|
+
configFiles: projects.map((project) => project.configFile),
|
|
429
|
+
fromScope: [],
|
|
430
|
+
};
|
|
303
431
|
}
|
|
304
432
|
|
|
305
433
|
/**
|
|
306
434
|
* The projects a run derives where the matrix declares none, in the order discovery
|
|
307
435
|
* reads them: the paths the scope names, then every compiler configuration under
|
|
308
436
|
* the target root that is not inside an ignored directory, then the configurations
|
|
309
|
-
* each of those references, transitively. Each configuration appears once.
|
|
437
|
+
* each of those references, transitively. Each configuration appears once. A
|
|
438
|
+
* configuration the scope names directly is the caller's own and ends the run where
|
|
439
|
+
* it cannot be read; one the walk found is dropped and named.
|
|
310
440
|
*/
|
|
311
441
|
function derivedProjects(engine: Engine, host: Host, scope: Scope): Discovery {
|
|
442
|
+
const targetRoot = scope.target.path;
|
|
312
443
|
const fromScope = scopeConfigFiles(host, scope);
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
444
|
+
const notBuilt: NotBuilt[] = [];
|
|
445
|
+
const configFiles = derivedFrom(
|
|
446
|
+
engine,
|
|
447
|
+
host,
|
|
448
|
+
[...fromScope, ...configFilesUnder(host, targetRoot)],
|
|
449
|
+
(configFile, reason) => {
|
|
450
|
+
if (fromScope.includes(configFile)) {
|
|
451
|
+
throw new DiscoveryError(
|
|
452
|
+
`${configFile} cannot be read as a compiler configuration: ${reason}`,
|
|
453
|
+
[],
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
notBuilt.push({
|
|
457
|
+
id: derivedId(targetRoot, configFile),
|
|
458
|
+
configFile,
|
|
459
|
+
error: targetRelative(reason, resolvePath(host.workingDirectory(), targetRoot)),
|
|
460
|
+
});
|
|
461
|
+
},
|
|
462
|
+
);
|
|
317
463
|
const projects = configFiles.map((configFile) => ({
|
|
318
|
-
id: derivedId(
|
|
464
|
+
id: derivedId(targetRoot, configFile),
|
|
319
465
|
configFile,
|
|
320
466
|
}));
|
|
321
|
-
return { projects, configFiles, fromScope };
|
|
467
|
+
return { projects, derived: true, notBuilt, configFiles, fromScope };
|
|
322
468
|
}
|
|
323
469
|
|
|
324
470
|
/**
|
|
@@ -331,12 +477,10 @@ function derivedProjects(engine: Engine, host: Host, scope: Scope): Discovery {
|
|
|
331
477
|
* scope and the target tree. A platform entry names a configuration of another
|
|
332
478
|
* language and plays no part here.
|
|
333
479
|
*
|
|
334
|
-
* A configuration file the compiler cannot read
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
* {@link diagnosticErrors} over the three sets a program reports, because a
|
|
339
|
-
* resolution carries no diagnostics.
|
|
480
|
+
* A configuration file the compiler cannot read is never analyzed. One the matrix or
|
|
481
|
+
* the scope names ends the run with the failure code; a guess is dropped and named in
|
|
482
|
+
* {@link Discovery.notBuilt}. What a configuration's own contents say is read once its
|
|
483
|
+
* project is open, by {@link diagnosticErrors}, because a resolution carries none.
|
|
340
484
|
*
|
|
341
485
|
* Nothing reads a build output: a project is the files its configuration names, so a
|
|
342
486
|
* package whose sources ship as TypeScript is analyzed as it stands.
|
package/src/entry-point-gaps.ts
CHANGED
|
@@ -249,7 +249,7 @@ const PARTLY_ENTERED: readonly DeclinedConvention[] = [
|
|
|
249
249
|
tool: "vite",
|
|
250
250
|
convention: "its configuration and every entry point it names",
|
|
251
251
|
reason:
|
|
252
|
-
"Only a worker a call addresses by a literal is entered
|
|
252
|
+
"Only a worker a call addresses by a literal is entered. No rule reads the " +
|
|
253
253
|
"configuration, so a file only it names is reported as never imported.",
|
|
254
254
|
},
|
|
255
255
|
{
|
|
@@ -284,7 +284,7 @@ const PARTLY_ENTERED: readonly DeclinedConvention[] = [
|
|
|
284
284
|
tool: "webpack",
|
|
285
285
|
convention: "its configuration and every entry point it names",
|
|
286
286
|
reason:
|
|
287
|
-
"Only a worker a call addresses by a literal is entered
|
|
287
|
+
"Only a worker a call addresses by a literal is entered. No rule reads the " +
|
|
288
288
|
"configuration, so a file only it names is reported as never imported.",
|
|
289
289
|
},
|
|
290
290
|
];
|
package/src/entry-points.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
import { SymbolFlags } from "@typescript/native/unstable/sync";
|
|
33
33
|
import { globExpression } from "./glob.ts";
|
|
34
34
|
import { dirnamePath, relativePath, resolvePath } from "./paths.ts";
|
|
35
|
+
import { UNANSWERED } from "./query.ts";
|
|
35
36
|
import type { ProjectView } from "./session.ts";
|
|
36
37
|
import type { SourceFiles } from "./source-files.ts";
|
|
37
38
|
|
|
@@ -435,8 +436,14 @@ function workersOf<Brand>(
|
|
|
435
436
|
const names = sites.flatMap((site) => site.platformNames);
|
|
436
437
|
const symbols = project.symbolsAt(names.map((name) => project.handle(name)));
|
|
437
438
|
const platform = new Set<Node>();
|
|
439
|
+
// A name the checker could not resolve is read as the platform's, so a worker it
|
|
440
|
+
// stands on is rooted rather than reported.
|
|
438
441
|
names.forEach((name, index) => {
|
|
439
442
|
const symbol = symbols[index];
|
|
443
|
+
if (symbol === UNANSWERED) {
|
|
444
|
+
platform.add(name);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
440
447
|
if (
|
|
441
448
|
symbol !== undefined &&
|
|
442
449
|
(symbol.flags & SymbolFlags.Alias) === 0 &&
|
package/src/enum-group.ts
CHANGED
|
@@ -33,7 +33,8 @@ import { aliasChains } from "./alias-chain.ts";
|
|
|
33
33
|
import type { DetectorInput, Evidence } from "./exempt.ts";
|
|
34
34
|
import { nodeKey } from "./inventory.ts";
|
|
35
35
|
import { renderPosition } from "./position.ts";
|
|
36
|
-
import {
|
|
36
|
+
import { isAnswered, UNANSWERED, type Answer } from "./query.ts";
|
|
37
|
+
import type { ProjectView } from "./session.ts";
|
|
37
38
|
|
|
38
39
|
/** The operand types a conversion produces an enum value from. */
|
|
39
40
|
const NUMBER = TypeFlags.NumberLike;
|
|
@@ -146,25 +147,38 @@ function sitesOf(file: SourceFile): readonly Site[] {
|
|
|
146
147
|
* union, and itself otherwise. An enum's own value, or one of its members, is a value the
|
|
147
148
|
* source names, whichever number or string it also is.
|
|
148
149
|
*/
|
|
149
|
-
function partsOf(type: Type): readonly Type[] {
|
|
150
|
-
const parts = type.isUnionType() ?
|
|
151
|
-
return parts
|
|
150
|
+
function partsOf<Brand>(project: ProjectView<Brand>, type: Type): Answer<readonly Type[]> {
|
|
151
|
+
const parts = type.isUnionType() ? project.queries.constituents(type) : [type];
|
|
152
|
+
return isAnswered(parts)
|
|
153
|
+
? parts.filter((part) => (part.flags & TypeFlags.EnumLike) === 0)
|
|
154
|
+
: parts;
|
|
152
155
|
}
|
|
153
156
|
|
|
157
|
+
/** The detail a site records whose operand's type the checker could not answer. */
|
|
158
|
+
const UNTYPED_DETAIL = "converted from a value of unanswered type";
|
|
159
|
+
|
|
154
160
|
/**
|
|
155
161
|
* The detail one site records about the enum it produces, or undefined where the
|
|
156
162
|
* operand's type converts nothing: a number, or a string an assertion converts, is
|
|
157
163
|
* converted, a value of type `any` or `unknown` is decoded, and a value of an enum is one
|
|
158
164
|
* the source names.
|
|
159
165
|
*/
|
|
160
|
-
function detailOf(
|
|
166
|
+
function detailOf<Brand>(
|
|
167
|
+
project: ProjectView<Brand>,
|
|
168
|
+
site: Site,
|
|
169
|
+
enumName: string,
|
|
170
|
+
operand: Answer<Type> | undefined,
|
|
171
|
+
): string | undefined {
|
|
161
172
|
if (site.form === "lookup") {
|
|
162
173
|
return `looked up by an element access on ${enumName}`;
|
|
163
174
|
}
|
|
164
175
|
if (operand === undefined) {
|
|
165
176
|
return undefined;
|
|
166
177
|
}
|
|
167
|
-
const parts = partsOf(operand);
|
|
178
|
+
const parts = operand === UNANSWERED ? UNANSWERED : partsOf(project, operand);
|
|
179
|
+
if (parts === UNANSWERED) {
|
|
180
|
+
return UNTYPED_DETAIL;
|
|
181
|
+
}
|
|
168
182
|
if (parts.some((part) => (part.flags & NUMBER) !== 0)) {
|
|
169
183
|
return "converted from number";
|
|
170
184
|
}
|
|
@@ -204,35 +218,37 @@ export function enumGroup<Brand>(input: DetectorInput<Brand>): readonly Evidence
|
|
|
204
218
|
}
|
|
205
219
|
|
|
206
220
|
const sites = files.flatMap(sitesOf);
|
|
207
|
-
const named = new Map<Site, string>();
|
|
221
|
+
const named = new Map<Site, readonly string[]>();
|
|
208
222
|
const chains = aliasChains(project, held);
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
223
|
+
// A name the checker could not resolve may stand for any enum of the target it spells.
|
|
224
|
+
const enumsNamed = (text: string): readonly string[] =>
|
|
225
|
+
[...enums].flatMap(([id, name]) => (name === text ? [id] : []));
|
|
226
|
+
project.symbolsAt(sites.map((site) => project.handle(site.name))).forEach((symbol, index) => {
|
|
227
|
+
const site = sites[index];
|
|
228
|
+
if (symbol === undefined || site === undefined) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (symbol === UNANSWERED) {
|
|
232
|
+
const ids = enumsNamed(site.name.getText());
|
|
233
|
+
if (ids.length > 0) {
|
|
234
|
+
named.set(site, ids);
|
|
219
235
|
}
|
|
220
|
-
|
|
221
|
-
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const target = chains.chainOf(symbol).find((link) => enums.has(link.id));
|
|
239
|
+
if (target !== undefined) {
|
|
240
|
+
named.set(site, [target.id]);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
222
243
|
|
|
223
244
|
const typed = sites.filter((site) => named.has(site) && site.operand !== undefined);
|
|
224
|
-
const operandType = new Map<Site, Type
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (type !== undefined && site !== undefined) {
|
|
232
|
-
operandType.set(site, type);
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
}
|
|
245
|
+
const operandType = new Map<Site, Answer<Type>>();
|
|
246
|
+
project.queries.typesAt(typed.map((site) => site.operand ?? site.at)).forEach((type, index) => {
|
|
247
|
+
const site = typed[index];
|
|
248
|
+
if (type !== undefined && site !== undefined) {
|
|
249
|
+
operandType.set(site, type);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
236
252
|
|
|
237
253
|
const members = new Map<string, string[]>();
|
|
238
254
|
for (const symbol of held.symbols) {
|
|
@@ -248,17 +264,15 @@ export function enumGroup<Brand>(input: DetectorInput<Brand>): readonly Evidence
|
|
|
248
264
|
|
|
249
265
|
const found: Evidence[] = [];
|
|
250
266
|
for (const site of sites) {
|
|
251
|
-
const enumId
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
for (const id of members.get(enumId) ?? []) {
|
|
261
|
-
found.push({ id, detail, site: position });
|
|
267
|
+
for (const enumId of named.get(site) ?? []) {
|
|
268
|
+
const detail = detailOf(project, site, enums.get(enumId) ?? "", operandType.get(site));
|
|
269
|
+
if (detail === undefined) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const position = renderPosition(site.at.getSourceFile(), targetRoot, site.at.getStart());
|
|
273
|
+
for (const id of members.get(enumId) ?? []) {
|
|
274
|
+
found.push({ id, detail, site: position });
|
|
275
|
+
}
|
|
262
276
|
}
|
|
263
277
|
}
|
|
264
278
|
return found;
|
package/src/exempt.ts
CHANGED
|
@@ -18,9 +18,10 @@ import {
|
|
|
18
18
|
type TSExemptionClass,
|
|
19
19
|
type TypeScriptVisibility,
|
|
20
20
|
} from "./exempt-classes.ts";
|
|
21
|
-
import type { Inventory, InventorySymbol, Visibility } from "./inventory.ts";
|
|
21
|
+
import type { Inventory, InventorySymbol, SymbolKind, Visibility } from "./inventory.ts";
|
|
22
22
|
import type { Matrix } from "./matrix.ts";
|
|
23
23
|
import { byPosition, positionKey, type Position } from "./position.ts";
|
|
24
|
+
import { Unanswerable } from "./query.ts";
|
|
24
25
|
import type { ProjectView } from "./session.ts";
|
|
25
26
|
import type { Templates } from "./template-field.ts";
|
|
26
27
|
import { sweep, type Exemption, type Mode, type SweepInput } from "./sweep.ts";
|
|
@@ -100,6 +101,53 @@ function retainable(allowed: TypeScriptVisibility, visibility: Visibility | unde
|
|
|
100
101
|
return true;
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
/**
|
|
105
|
+
* The kinds of declaration each class can hold back, which a detector that stops holds
|
|
106
|
+
* back whole. A class with no row here holds back every declaration but a file.
|
|
107
|
+
*/
|
|
108
|
+
const RETAINABLE: ReadonlyMap<TSExemptionClass, ReadonlySet<SymbolKind>> = new Map([
|
|
109
|
+
["interface-satisfaction", new Set<SymbolKind>(["method", "class-member"])],
|
|
110
|
+
["enum-group", new Set<SymbolKind>(["enum-member"])],
|
|
111
|
+
["injection-container", new Set<SymbolKind>(["method", "class-member"])],
|
|
112
|
+
["framework-lifecycle", new Set<SymbolKind>(["method", "class-member"])],
|
|
113
|
+
[
|
|
114
|
+
"serialization-contract",
|
|
115
|
+
new Set<SymbolKind>(["method", "class-member", "interface-method", "type-member"]),
|
|
116
|
+
],
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
/** The detail of a record a detector that could not finish holds. */
|
|
120
|
+
const UNANSWERED_DETAIL = "kept live by a question the checker did not answer";
|
|
121
|
+
|
|
122
|
+
/** Whether one record is held by a detector that could not finish. */
|
|
123
|
+
export function isUnansweredRecord(record: Exemption): boolean {
|
|
124
|
+
return record.detail === UNANSWERED_DETAIL;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* What one detector holds back over one project. A detector that stops because the
|
|
129
|
+
* checker left a question it needs unanswered holds back every declaration of the
|
|
130
|
+
* project its class could hold back, each recorded at its own position, so the gap
|
|
131
|
+
* never yields a finding.
|
|
132
|
+
*/
|
|
133
|
+
function detected<Brand>(
|
|
134
|
+
exemptionClass: TSExemptionClass,
|
|
135
|
+
detect: Detector,
|
|
136
|
+
input: DetectorInput<Brand>,
|
|
137
|
+
): readonly Evidence[] {
|
|
138
|
+
try {
|
|
139
|
+
return detect(input);
|
|
140
|
+
} catch (error: unknown) {
|
|
141
|
+
if (!(error instanceof Unanswerable)) {
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
const kinds = RETAINABLE.get(exemptionClass);
|
|
145
|
+
return input.held.symbols
|
|
146
|
+
.filter((symbol) => (kinds === undefined ? symbol.kind !== "file" : kinds.has(symbol.kind)))
|
|
147
|
+
.map((symbol) => ({ id: symbol.id, detail: UNANSWERED_DETAIL, site: symbol.position }));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
103
151
|
/**
|
|
104
152
|
* The records one project's detectors found, each class in the vocabulary's order and
|
|
105
153
|
* none of a switched-off class. Evidence on a private member the class's row says it
|
|
@@ -120,7 +168,7 @@ export function computeExemptions<Brand>(
|
|
|
120
168
|
continue;
|
|
121
169
|
}
|
|
122
170
|
const allowed = typescriptVisibilityOf(exemptionClass);
|
|
123
|
-
for (const evidence of detect
|
|
171
|
+
for (const evidence of detected(exemptionClass, detect, input)) {
|
|
124
172
|
if (!retainable(allowed, visibility.get(evidence.id))) {
|
|
125
173
|
continue;
|
|
126
174
|
}
|
package/src/findings/emitters.ts
CHANGED
|
@@ -24,6 +24,22 @@ export const EMITTERS: Emitters = new Map<string, Emitter>([
|
|
|
24
24
|
["intra-function", intraFunction],
|
|
25
25
|
]);
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* The references of every declaration an unanswered question could have kept live, which
|
|
29
|
+
* the run reports nothing about: a finding about one would be computed from the gap.
|
|
30
|
+
*/
|
|
31
|
+
function heldRefs(input: EmitterInput): ReadonlySet<string> {
|
|
32
|
+
const held = new Set(input.swept.heldByUnanswered);
|
|
33
|
+
if (held.size === 0) {
|
|
34
|
+
return held;
|
|
35
|
+
}
|
|
36
|
+
return new Set(
|
|
37
|
+
input.swept.matrix.union.symbols
|
|
38
|
+
.filter((symbol) => held.has(symbol.id))
|
|
39
|
+
.map((symbol) => symbol.ref),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
/** What the suppressions of a run swept under their marks are decided against. */
|
|
28
44
|
export interface Suppressed {
|
|
29
45
|
/** The same run swept with no mark, which is where a kind about liveness reports a marked declaration. */
|
|
@@ -92,9 +108,10 @@ export function decidedFindings(
|
|
|
92
108
|
suppressed.records.length === 0 ? [] : completedOver(suppressed.unmarked, fallenMembers);
|
|
93
109
|
const would = [...unmarked, ...found];
|
|
94
110
|
const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
|
|
111
|
+
const unanswered = heldRefs(input);
|
|
95
112
|
const kept = reportable(
|
|
96
113
|
input,
|
|
97
|
-
found.filter((finding) => !ledger.withheld(finding)),
|
|
114
|
+
found.filter((finding) => !ledger.withheld(finding) && !unanswered.has(finding.symbol.ref)),
|
|
98
115
|
);
|
|
99
116
|
const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
|
|
100
117
|
return {
|
|
@@ -131,8 +131,14 @@ function unusedMessage(candidate: Candidate): string {
|
|
|
131
131
|
: UNUSED_MESSAGE;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
/**
|
|
134
|
+
/**
|
|
135
|
+
* Whether one interface method has implementations and every one has an empty body, or
|
|
136
|
+
* is one whose implementations the run could not read in full.
|
|
137
|
+
*/
|
|
135
138
|
function marker(input: EmitterInput, method: string): boolean {
|
|
139
|
+
if (input.implementations.unknown.has(method)) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
136
142
|
const bodies = [...(input.implementations.bodies.get(method)?.values() ?? [])];
|
|
137
143
|
return bodies.length > 0 && bodies.every((empty) => empty);
|
|
138
144
|
}
|