@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/dist/src/discover.js
CHANGED
|
@@ -19,14 +19,35 @@ export class DiscoveryError extends Error {
|
|
|
19
19
|
this.diagnostics = diagnostics;
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
+
/** The category an error diagnostic carries, which the protocol sends as a number. */
|
|
23
|
+
const ERROR_CATEGORY = DiagnosticCategory.Error;
|
|
22
24
|
/** Whether one diagnostic is an error, which is what fails a run closed. */
|
|
23
25
|
export function isError(diagnostic) {
|
|
24
|
-
return diagnostic.category ===
|
|
26
|
+
return diagnostic.category === ERROR_CATEGORY;
|
|
25
27
|
}
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
/**
|
|
29
|
+
* One diagnostic as a line naming its file, its line and column, its code and its
|
|
30
|
+
* message: `path:line:column: TScode: message`, the position left out where the
|
|
31
|
+
* diagnostic names none and the file where it names none. `display` spells the
|
|
32
|
+
* file's path; it answers the path itself where it is not given.
|
|
33
|
+
*/
|
|
34
|
+
export function renderDiagnostic(located, display = (path) => path) {
|
|
35
|
+
const { diagnostic, at } = located;
|
|
36
|
+
const message = `TS${String(diagnostic.code)}: ${diagnostic.text}`;
|
|
37
|
+
if (diagnostic.fileName === undefined) {
|
|
38
|
+
return message;
|
|
39
|
+
}
|
|
40
|
+
const where = display(diagnostic.fileName);
|
|
41
|
+
return at === undefined
|
|
42
|
+
? `${where}: ${message}`
|
|
43
|
+
: `${where}:${String(at.line)}:${String(at.column)}: ${message}`;
|
|
44
|
+
}
|
|
45
|
+
/** Each diagnostic with the line and column its offset falls on. */
|
|
46
|
+
function locatedIn(diagnostics, locate) {
|
|
47
|
+
return diagnostics.map((diagnostic) => ({
|
|
48
|
+
diagnostic,
|
|
49
|
+
at: diagnostic.fileName === undefined ? undefined : locate(diagnostic.fileName, diagnostic.pos),
|
|
50
|
+
}));
|
|
30
51
|
}
|
|
31
52
|
/**
|
|
32
53
|
* Every error a project's three diagnostic sets carry, in the order the compiler
|
|
@@ -38,6 +59,32 @@ export function renderDiagnostic(diagnostic) {
|
|
|
38
59
|
export function diagnosticErrors(sets) {
|
|
39
60
|
return [...sets.configParsing, ...sets.syntactic, ...sets.semantic].filter(isError);
|
|
40
61
|
}
|
|
62
|
+
/** The errors one open project carries, each located in the file it names. */
|
|
63
|
+
export function projectErrors(sets) {
|
|
64
|
+
const load = sets.configParsing.filter(isError);
|
|
65
|
+
return {
|
|
66
|
+
load: locatedIn(load, sets.locate),
|
|
67
|
+
check: load.length > 0 ? [] : locatedIn(diagnosticErrors(sets), sets.locate),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The first line of one error about the target, every mention of the target root
|
|
72
|
+
* spelled relative to it: a path below the root loses the root and its separator, and
|
|
73
|
+
* the root itself is `.`.
|
|
74
|
+
*/
|
|
75
|
+
export function targetRelative(text, targetRoot) {
|
|
76
|
+
const root = targetRoot.endsWith("/") ? targetRoot.slice(0, -1) : targetRoot;
|
|
77
|
+
const [first = ""] = text.split(/\r?\n/u);
|
|
78
|
+
const spelled = first.split(`${root}/`).join("").split(root).join(".").trim();
|
|
79
|
+
return spelled === "" ? "the configuration could not be built" : spelled;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Whether one configuration is the analyzer's own guess: discovery derived it and the
|
|
83
|
+
* scope did not name it. A guess that does not load is dropped; any other ends the run.
|
|
84
|
+
*/
|
|
85
|
+
export function isGuess(discovery, configFile) {
|
|
86
|
+
return discovery.derived && !discovery.fromScope.includes(configFile);
|
|
87
|
+
}
|
|
41
88
|
function isRecord(value) {
|
|
42
89
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
43
90
|
}
|
|
@@ -130,9 +177,11 @@ function scopeConfigFiles(host, scope) {
|
|
|
130
177
|
/**
|
|
131
178
|
* Every configuration file reached from `pending`, in the order discovery reads them:
|
|
132
179
|
* each once, each followed by the configurations it references, transitively. A
|
|
133
|
-
* configuration the compiler cannot read ends the run naming it
|
|
180
|
+
* configuration the compiler cannot read ends the run naming it, unless `unreadable`
|
|
181
|
+
* is given, which is then told the configuration and why, and the walk goes on
|
|
182
|
+
* without it.
|
|
134
183
|
*/
|
|
135
|
-
function derivedFrom(engine, host, pending) {
|
|
184
|
+
function derivedFrom(engine, host, pending, unreadable) {
|
|
136
185
|
const seen = new Set();
|
|
137
186
|
const found = [];
|
|
138
187
|
while (pending.length > 0) {
|
|
@@ -145,7 +194,14 @@ function derivedFrom(engine, host, pending) {
|
|
|
145
194
|
continue;
|
|
146
195
|
}
|
|
147
196
|
seen.add(path);
|
|
148
|
-
|
|
197
|
+
const reason = unreadableReason(engine, path);
|
|
198
|
+
if (reason !== undefined) {
|
|
199
|
+
if (unreadable === undefined) {
|
|
200
|
+
throw new DiscoveryError(`${path} cannot be read as a compiler configuration: ${reason}`, []);
|
|
201
|
+
}
|
|
202
|
+
unreadable(path, reason);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
149
205
|
found.push(path);
|
|
150
206
|
pending.push(...referencesOf(host, path));
|
|
151
207
|
}
|
|
@@ -179,18 +235,25 @@ export function consumerProjects(engine, host, root) {
|
|
|
179
235
|
function derivedId(targetRoot, configFile) {
|
|
180
236
|
return relativePath(targetRoot, configFile) ?? configFile;
|
|
181
237
|
}
|
|
238
|
+
/** Why the compiler cannot read one configuration file, or undefined where it can. */
|
|
239
|
+
function unreadableReason(engine, configFile) {
|
|
240
|
+
try {
|
|
241
|
+
engine.parseConfigFile(configFile);
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
return error instanceof Error ? error.message : String(error);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
182
248
|
/**
|
|
183
249
|
* Resolves one compiler configuration file, or ends the run naming it: a project
|
|
184
250
|
* opened from a configuration the compiler cannot read would be analyzed with no
|
|
185
251
|
* type information.
|
|
186
252
|
*/
|
|
187
253
|
function parseOrRefuse(engine, configFile, named) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
catch (error) {
|
|
192
|
-
throw new DiscoveryError(`${named} cannot be read as a compiler configuration: ` +
|
|
193
|
-
(error instanceof Error ? error.message : String(error)), []);
|
|
254
|
+
const reason = unreadableReason(engine, configFile);
|
|
255
|
+
if (reason !== undefined) {
|
|
256
|
+
throw new DiscoveryError(`${named} cannot be read as a compiler configuration: ${reason}`, []);
|
|
194
257
|
}
|
|
195
258
|
}
|
|
196
259
|
/**
|
|
@@ -232,25 +295,41 @@ function declaredProjects(engine, host, targetRoot, entries) {
|
|
|
232
295
|
"into the program that references it, so the matrix names it too", []);
|
|
233
296
|
}
|
|
234
297
|
}
|
|
235
|
-
return {
|
|
298
|
+
return {
|
|
299
|
+
projects,
|
|
300
|
+
derived: false,
|
|
301
|
+
notBuilt: [],
|
|
302
|
+
configFiles: projects.map((project) => project.configFile),
|
|
303
|
+
fromScope: [],
|
|
304
|
+
};
|
|
236
305
|
}
|
|
237
306
|
/**
|
|
238
307
|
* The projects a run derives where the matrix declares none, in the order discovery
|
|
239
308
|
* reads them: the paths the scope names, then every compiler configuration under
|
|
240
309
|
* the target root that is not inside an ignored directory, then the configurations
|
|
241
|
-
* each of those references, transitively. Each configuration appears once.
|
|
310
|
+
* each of those references, transitively. Each configuration appears once. A
|
|
311
|
+
* configuration the scope names directly is the caller's own and ends the run where
|
|
312
|
+
* it cannot be read; one the walk found is dropped and named.
|
|
242
313
|
*/
|
|
243
314
|
function derivedProjects(engine, host, scope) {
|
|
315
|
+
const targetRoot = scope.target.path;
|
|
244
316
|
const fromScope = scopeConfigFiles(host, scope);
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
317
|
+
const notBuilt = [];
|
|
318
|
+
const configFiles = derivedFrom(engine, host, [...fromScope, ...configFilesUnder(host, targetRoot)], (configFile, reason) => {
|
|
319
|
+
if (fromScope.includes(configFile)) {
|
|
320
|
+
throw new DiscoveryError(`${configFile} cannot be read as a compiler configuration: ${reason}`, []);
|
|
321
|
+
}
|
|
322
|
+
notBuilt.push({
|
|
323
|
+
id: derivedId(targetRoot, configFile),
|
|
324
|
+
configFile,
|
|
325
|
+
error: targetRelative(reason, resolvePath(host.workingDirectory(), targetRoot)),
|
|
326
|
+
});
|
|
327
|
+
});
|
|
249
328
|
const projects = configFiles.map((configFile) => ({
|
|
250
|
-
id: derivedId(
|
|
329
|
+
id: derivedId(targetRoot, configFile),
|
|
251
330
|
configFile,
|
|
252
331
|
}));
|
|
253
|
-
return { projects, configFiles, fromScope };
|
|
332
|
+
return { projects, derived: true, notBuilt, configFiles, fromScope };
|
|
254
333
|
}
|
|
255
334
|
/**
|
|
256
335
|
* Every project one run analyzes.
|
|
@@ -262,12 +341,10 @@ function derivedProjects(engine, host, scope) {
|
|
|
262
341
|
* scope and the target tree. A platform entry names a configuration of another
|
|
263
342
|
* language and plays no part here.
|
|
264
343
|
*
|
|
265
|
-
* A configuration file the compiler cannot read
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
* {@link diagnosticErrors} over the three sets a program reports, because a
|
|
270
|
-
* resolution carries no diagnostics.
|
|
344
|
+
* A configuration file the compiler cannot read is never analyzed. One the matrix or
|
|
345
|
+
* the scope names ends the run with the failure code; a guess is dropped and named in
|
|
346
|
+
* {@link Discovery.notBuilt}. What a configuration's own contents say is read once its
|
|
347
|
+
* project is open, by {@link diagnosticErrors}, because a resolution carries none.
|
|
271
348
|
*
|
|
272
349
|
* Nothing reads a build output: a project is the files its configuration names, so a
|
|
273
350
|
* package whose sources ship as TypeScript is analyzed as it stands.
|
package/dist/src/edges.js
CHANGED
|
@@ -8,6 +8,7 @@ import { ConfigError } from "./config.js";
|
|
|
8
8
|
import { positionAt, walkDocument } from "./json-document.js";
|
|
9
9
|
import { joinPath } from "./paths.js";
|
|
10
10
|
import { positionKey } from "./position.js";
|
|
11
|
+
import { isPartSubject } from "./suppress.js";
|
|
11
12
|
/** The name and the location the Contract fixes for the edges document. */
|
|
12
13
|
export const EDGES_FILE = "deadset-edges.json";
|
|
13
14
|
/** The language prefix of every reference this analyzer evaluates. */
|
|
@@ -169,7 +170,9 @@ export function evaluateEdges(findings, sides, symbols) {
|
|
|
169
170
|
}
|
|
170
171
|
const held = new Map();
|
|
171
172
|
findings.forEach((finding, at) => {
|
|
172
|
-
|
|
173
|
+
if (!isPartSubject(finding.symbol.kind)) {
|
|
174
|
+
held.set(positionKey(finding.position), at);
|
|
175
|
+
}
|
|
173
176
|
});
|
|
174
177
|
const pending = new Set();
|
|
175
178
|
const evaluations = sides.map((side) => {
|
package/dist/src/entry-points.js
CHANGED
|
@@ -14,6 +14,7 @@ import { isArrayLiteralExpression, isAsExpression, isCallExpression, isExportAss
|
|
|
14
14
|
import { SymbolFlags } from "@typescript/native/unstable/sync";
|
|
15
15
|
import { globExpression } from "./glob.js";
|
|
16
16
|
import { dirnamePath, relativePath, resolvePath } from "./paths.js";
|
|
17
|
+
import { UNANSWERED } from "./query.js";
|
|
17
18
|
/** The extensions the test runner and the browser-test runner load a configuration from. */
|
|
18
19
|
const MODULE_EXTENSIONS = ["js", "mjs", "cjs", "ts", "mts", "cts"];
|
|
19
20
|
/** The extensions the linter's flat configuration and the mutation tester's are loaded from. */
|
|
@@ -331,8 +332,14 @@ function workersOf(project, file, files) {
|
|
|
331
332
|
const names = sites.flatMap((site) => site.platformNames);
|
|
332
333
|
const symbols = project.symbolsAt(names.map((name) => project.handle(name)));
|
|
333
334
|
const platform = new Set();
|
|
335
|
+
// A name the checker could not resolve is read as the platform's, so a worker it
|
|
336
|
+
// stands on is rooted rather than reported.
|
|
334
337
|
names.forEach((name, index) => {
|
|
335
338
|
const symbol = symbols[index];
|
|
339
|
+
if (symbol === UNANSWERED) {
|
|
340
|
+
platform.add(name);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
336
343
|
if (symbol !== undefined &&
|
|
337
344
|
(symbol.flags & SymbolFlags.Alias) === 0 &&
|
|
338
345
|
symbol.declarations.length > 0 &&
|
package/dist/src/enum-group.js
CHANGED
|
@@ -11,7 +11,7 @@ import { TypeFlags } from "@typescript/native/unstable/sync";
|
|
|
11
11
|
import { aliasChains } from "./alias-chain.js";
|
|
12
12
|
import { nodeKey } from "./inventory.js";
|
|
13
13
|
import { renderPosition } from "./position.js";
|
|
14
|
-
import {
|
|
14
|
+
import { isAnswered, UNANSWERED } from "./query.js";
|
|
15
15
|
/** The operand types a conversion produces an enum value from. */
|
|
16
16
|
const NUMBER = TypeFlags.NumberLike;
|
|
17
17
|
const STRING = TypeFlags.StringLike;
|
|
@@ -104,24 +104,31 @@ function sitesOf(file) {
|
|
|
104
104
|
* union, and itself otherwise. An enum's own value, or one of its members, is a value the
|
|
105
105
|
* source names, whichever number or string it also is.
|
|
106
106
|
*/
|
|
107
|
-
function partsOf(type) {
|
|
108
|
-
const parts = type.isUnionType() ?
|
|
109
|
-
return parts
|
|
107
|
+
function partsOf(project, type) {
|
|
108
|
+
const parts = type.isUnionType() ? project.queries.constituents(type) : [type];
|
|
109
|
+
return isAnswered(parts)
|
|
110
|
+
? parts.filter((part) => (part.flags & TypeFlags.EnumLike) === 0)
|
|
111
|
+
: parts;
|
|
110
112
|
}
|
|
113
|
+
/** The detail a site records whose operand's type the checker could not answer. */
|
|
114
|
+
const UNTYPED_DETAIL = "converted from a value of unanswered type";
|
|
111
115
|
/**
|
|
112
116
|
* The detail one site records about the enum it produces, or undefined where the
|
|
113
117
|
* operand's type converts nothing: a number, or a string an assertion converts, is
|
|
114
118
|
* converted, a value of type `any` or `unknown` is decoded, and a value of an enum is one
|
|
115
119
|
* the source names.
|
|
116
120
|
*/
|
|
117
|
-
function detailOf(site, enumName, operand) {
|
|
121
|
+
function detailOf(project, site, enumName, operand) {
|
|
118
122
|
if (site.form === "lookup") {
|
|
119
123
|
return `looked up by an element access on ${enumName}`;
|
|
120
124
|
}
|
|
121
125
|
if (operand === undefined) {
|
|
122
126
|
return undefined;
|
|
123
127
|
}
|
|
124
|
-
const parts = partsOf(operand);
|
|
128
|
+
const parts = operand === UNANSWERED ? UNANSWERED : partsOf(project, operand);
|
|
129
|
+
if (parts === UNANSWERED) {
|
|
130
|
+
return UNTYPED_DETAIL;
|
|
131
|
+
}
|
|
125
132
|
if (parts.some((part) => (part.flags & NUMBER) !== 0)) {
|
|
126
133
|
return "converted from number";
|
|
127
134
|
}
|
|
@@ -160,32 +167,33 @@ export function enumGroup(input) {
|
|
|
160
167
|
const sites = files.flatMap(sitesOf);
|
|
161
168
|
const named = new Map();
|
|
162
169
|
const chains = aliasChains(project, held);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
// A name the checker could not resolve may stand for any enum of the target it spells.
|
|
171
|
+
const enumsNamed = (text) => [...enums].flatMap(([id, name]) => (name === text ? [id] : []));
|
|
172
|
+
project.symbolsAt(sites.map((site) => project.handle(site.name))).forEach((symbol, index) => {
|
|
173
|
+
const site = sites[index];
|
|
174
|
+
if (symbol === undefined || site === undefined) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (symbol === UNANSWERED) {
|
|
178
|
+
const ids = enumsNamed(site.name.getText());
|
|
179
|
+
if (ids.length > 0) {
|
|
180
|
+
named.set(site, ids);
|
|
173
181
|
}
|
|
174
|
-
|
|
175
|
-
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const target = chains.chainOf(symbol).find((link) => enums.has(link.id));
|
|
185
|
+
if (target !== undefined) {
|
|
186
|
+
named.set(site, [target.id]);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
176
189
|
const typed = sites.filter((site) => named.has(site) && site.operand !== undefined);
|
|
177
190
|
const operandType = new Map();
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (type !== undefined && site !== undefined) {
|
|
185
|
-
operandType.set(site, type);
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
}
|
|
191
|
+
project.queries.typesAt(typed.map((site) => site.operand ?? site.at)).forEach((type, index) => {
|
|
192
|
+
const site = typed[index];
|
|
193
|
+
if (type !== undefined && site !== undefined) {
|
|
194
|
+
operandType.set(site, type);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
189
197
|
const members = new Map();
|
|
190
198
|
for (const symbol of held.symbols) {
|
|
191
199
|
if (symbol.kind === "enum-member" && enums.has(symbol.parent)) {
|
|
@@ -200,17 +208,15 @@ export function enumGroup(input) {
|
|
|
200
208
|
}
|
|
201
209
|
const found = [];
|
|
202
210
|
for (const site of sites) {
|
|
203
|
-
const enumId
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
for (const id of members.get(enumId) ?? []) {
|
|
213
|
-
found.push({ id, detail, site: position });
|
|
211
|
+
for (const enumId of named.get(site) ?? []) {
|
|
212
|
+
const detail = detailOf(project, site, enums.get(enumId) ?? "", operandType.get(site));
|
|
213
|
+
if (detail === undefined) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const position = renderPosition(site.at.getSourceFile(), targetRoot, site.at.getStart());
|
|
217
|
+
for (const id of members.get(enumId) ?? []) {
|
|
218
|
+
found.push({ id, detail, site: position });
|
|
219
|
+
}
|
|
214
220
|
}
|
|
215
221
|
}
|
|
216
222
|
return found;
|
package/dist/src/exempt.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { ConfigError } from "./config.js";
|
|
10
10
|
import { EXEMPTION_CLASSES, isExemptionClass, TS_EXEMPTION_CLASSES, typescriptVisibilityOf, } from "./exempt-classes.js";
|
|
11
11
|
import { byPosition, positionKey } from "./position.js";
|
|
12
|
+
import { Unanswerable } from "./query.js";
|
|
12
13
|
import { sweep } from "./sweep.js";
|
|
13
14
|
/**
|
|
14
15
|
* The classes `exemptions.disabled` switches off. A name is refused unless the
|
|
@@ -39,6 +40,46 @@ function retainable(allowed, visibility) {
|
|
|
39
40
|
}
|
|
40
41
|
return true;
|
|
41
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* The kinds of declaration each class can hold back, which a detector that stops holds
|
|
45
|
+
* back whole. A class with no row here holds back every declaration but a file.
|
|
46
|
+
*/
|
|
47
|
+
const RETAINABLE = new Map([
|
|
48
|
+
["interface-satisfaction", new Set(["method", "class-member"])],
|
|
49
|
+
["enum-group", new Set(["enum-member"])],
|
|
50
|
+
["injection-container", new Set(["method", "class-member"])],
|
|
51
|
+
["framework-lifecycle", new Set(["method", "class-member"])],
|
|
52
|
+
[
|
|
53
|
+
"serialization-contract",
|
|
54
|
+
new Set(["method", "class-member", "interface-method", "type-member"]),
|
|
55
|
+
],
|
|
56
|
+
]);
|
|
57
|
+
/** The detail of a record a detector that could not finish holds. */
|
|
58
|
+
const UNANSWERED_DETAIL = "kept live by a question the checker did not answer";
|
|
59
|
+
/** Whether one record is held by a detector that could not finish. */
|
|
60
|
+
export function isUnansweredRecord(record) {
|
|
61
|
+
return record.detail === UNANSWERED_DETAIL;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* What one detector holds back over one project. A detector that stops because the
|
|
65
|
+
* checker left a question it needs unanswered holds back every declaration of the
|
|
66
|
+
* project its class could hold back, each recorded at its own position, so the gap
|
|
67
|
+
* never yields a finding.
|
|
68
|
+
*/
|
|
69
|
+
function detected(exemptionClass, detect, input) {
|
|
70
|
+
try {
|
|
71
|
+
return detect(input);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (!(error instanceof Unanswerable)) {
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
const kinds = RETAINABLE.get(exemptionClass);
|
|
78
|
+
return input.held.symbols
|
|
79
|
+
.filter((symbol) => (kinds === undefined ? symbol.kind !== "file" : kinds.has(symbol.kind)))
|
|
80
|
+
.map((symbol) => ({ id: symbol.id, detail: UNANSWERED_DETAIL, site: symbol.position }));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
42
83
|
/**
|
|
43
84
|
* The records one project's detectors found, each class in the vocabulary's order and
|
|
44
85
|
* none of a switched-off class. Evidence on a private member the class's row says it
|
|
@@ -55,7 +96,7 @@ export function computeExemptions(input, detectors, holding) {
|
|
|
55
96
|
continue;
|
|
56
97
|
}
|
|
57
98
|
const allowed = typescriptVisibilityOf(exemptionClass);
|
|
58
|
-
for (const evidence of detect
|
|
99
|
+
for (const evidence of detected(exemptionClass, detect, input)) {
|
|
59
100
|
if (!retainable(allowed, visibility.get(evidence.id))) {
|
|
60
101
|
continue;
|
|
61
102
|
}
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
import { componentId } from "../components.js";
|
|
7
7
|
import { OUTSIDE } from "../graph.js";
|
|
8
8
|
import { KINDS } from "../kinds.js";
|
|
9
|
+
import { byPosition } from "../position.js";
|
|
9
10
|
import { FAMILY_KEY_LENGTH } from "../resolve.js";
|
|
10
|
-
import { isRowSubject } from "../suppress.js";
|
|
11
|
+
import { isPartSubject, isRowSubject } from "../suppress.js";
|
|
11
12
|
/** The classes ranked by the claim each makes, the strongest highest. */
|
|
12
13
|
const RANK = { certain: 3, probable: 2, possible: 1 };
|
|
13
14
|
/** The weaker of two classes, which is a class capped by a ceiling. */
|
|
@@ -66,14 +67,35 @@ function reachabilityClasses(input) {
|
|
|
66
67
|
: "probable";
|
|
67
68
|
return (id) => (published.has(id) ? outside : "certain");
|
|
68
69
|
}
|
|
69
|
-
/**
|
|
70
|
-
|
|
70
|
+
/**
|
|
71
|
+
* The component each dead declaration of the run falls in, and none for a live one, with
|
|
72
|
+
* its members listed in the canonical order where the run lists a component in full.
|
|
73
|
+
*/
|
|
74
|
+
function findingComponents(swept, cascade) {
|
|
71
75
|
const held = new Map();
|
|
72
76
|
for (const component of swept.sweep.components) {
|
|
73
77
|
for (const member of component.members) {
|
|
74
78
|
held.set(member, component);
|
|
75
79
|
}
|
|
76
80
|
}
|
|
81
|
+
const symbols = new Map(swept.matrix.union.symbols.map((symbol) => [symbol.id, symbol]));
|
|
82
|
+
const listed = (component) => component.members
|
|
83
|
+
.flatMap((member) => {
|
|
84
|
+
const symbol = symbols.get(member);
|
|
85
|
+
return symbol === undefined
|
|
86
|
+
? []
|
|
87
|
+
: [
|
|
88
|
+
{
|
|
89
|
+
ref: symbol.ref,
|
|
90
|
+
name: symbol.name,
|
|
91
|
+
position: {
|
|
92
|
+
...symbol.position,
|
|
93
|
+
endLine: Math.max(symbol.position.line, symbol.endLine),
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
})
|
|
98
|
+
.sort((a, b) => byPosition(a.position, b.position) || (a.ref === b.ref ? 0 : a.ref < b.ref ? -1 : 1));
|
|
77
99
|
return (id) => {
|
|
78
100
|
const component = held.get(id);
|
|
79
101
|
return component === undefined
|
|
@@ -83,6 +105,7 @@ function findingComponents(swept) {
|
|
|
83
105
|
root: component.roots.includes(id),
|
|
84
106
|
symbolCount: component.members.length,
|
|
85
107
|
deletableLines: component.deletableLines,
|
|
108
|
+
...(cascade === "full" ? { members: listed(component) } : {}),
|
|
86
109
|
};
|
|
87
110
|
};
|
|
88
111
|
}
|
|
@@ -101,7 +124,7 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
101
124
|
const positions = new Map(union.symbols.map((symbol, at) => [symbol.ref, at]));
|
|
102
125
|
const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
|
|
103
126
|
const classOf = reachabilityClasses(input);
|
|
104
|
-
const componentOf = findingComponents(swept);
|
|
127
|
+
const componentOf = findingComponents(swept, input.config.reporters.cascade);
|
|
105
128
|
let minted = swept.sweep.components.length;
|
|
106
129
|
return findings.map((finding) => {
|
|
107
130
|
const row = kinds.get(finding.code);
|
|
@@ -113,11 +136,15 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
113
136
|
const at = isRowSubject(finding.symbol.kind)
|
|
114
137
|
? OUTSIDE
|
|
115
138
|
: (positions.get(finding.symbol.ref) ?? OUTSIDE);
|
|
139
|
+
// A part names the declaration that holds it, and is decided inside it rather than by
|
|
140
|
+
// the declaration's liveness, so it carries none of the declaration's sweep facts.
|
|
141
|
+
const part = isPartSubject(finding.symbol.kind);
|
|
116
142
|
const symbol = union.symbols[at];
|
|
117
|
-
const judged = union.subject[at] === true ? symbol : undefined;
|
|
143
|
+
const judged = !part && union.subject[at] === true ? symbol : undefined;
|
|
118
144
|
const candidate = judged === undefined ? undefined : candidates.get(judged.id);
|
|
119
|
-
const found = symbol === undefined ? undefined : componentOf(symbol.id);
|
|
120
|
-
|
|
145
|
+
const found = symbol === undefined || part ? undefined : componentOf(symbol.id);
|
|
146
|
+
// A test file's declaration is a component member only as a test of dead code.
|
|
147
|
+
if (candidate !== undefined && found === undefined && union.test[at] !== true) {
|
|
121
148
|
throw new Error(`${candidate.id} is judged dead and falls in no dead component`);
|
|
122
149
|
}
|
|
123
150
|
let component = found;
|
|
@@ -148,7 +175,9 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
148
175
|
component,
|
|
149
176
|
retainedBy: [],
|
|
150
177
|
configurations: candidate?.configurations ??
|
|
151
|
-
(judged === undefined
|
|
178
|
+
(judged === undefined && !part
|
|
179
|
+
? swept.matrix.configurations
|
|
180
|
+
: (swept.matrix.heldIn[at] ?? swept.matrix.configurations)),
|
|
152
181
|
consumersLoaded: input.boundary.consumers.loaded,
|
|
153
182
|
fixability: row.fixability,
|
|
154
183
|
severity: severityOf(input, finding.code, row.defaultSeverity),
|
|
@@ -7,7 +7,7 @@ import { intraFunction } from "./intra-function.js";
|
|
|
7
7
|
import { nonCodeArtifacts } from "./non-code-artifacts.js";
|
|
8
8
|
import { readsAndWrites } from "./reads-and-writes.js";
|
|
9
9
|
import { selfCheck } from "./self-check.js";
|
|
10
|
-
import { unusedDeclarations } from "./unused-declarations.js";
|
|
10
|
+
import { fallenMembers, unusedDeclarations } from "./unused-declarations.js";
|
|
11
11
|
import { visibilityNarrowing } from "./visibility-narrowing.js";
|
|
12
12
|
/** Every kind family this analyzer reports, each by its emitter, in code order. */
|
|
13
13
|
export const EMITTERS = new Map([
|
|
@@ -20,9 +20,22 @@ export const EMITTERS = new Map([
|
|
|
20
20
|
["self-check", selfCheck],
|
|
21
21
|
["intra-function", intraFunction],
|
|
22
22
|
]);
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
/**
|
|
24
|
+
* The references of every declaration an unanswered question could have kept live, which
|
|
25
|
+
* the run reports nothing about: a finding about one would be computed from the gap.
|
|
26
|
+
*/
|
|
27
|
+
function heldRefs(input) {
|
|
28
|
+
const held = new Set(input.swept.heldByUnanswered);
|
|
29
|
+
if (held.size === 0) {
|
|
30
|
+
return held;
|
|
31
|
+
}
|
|
32
|
+
return new Set(input.swept.matrix.union.symbols
|
|
33
|
+
.filter((symbol) => held.has(symbol.id))
|
|
34
|
+
.map((symbol) => symbol.ref));
|
|
35
|
+
}
|
|
36
|
+
/** Every family's findings over one sweep, and those of any further emitter, completed by one step. */
|
|
37
|
+
function completedOver(input, ...more) {
|
|
38
|
+
return completed(input, [...EMITTERS.values(), ...more].flatMap((emit) => emit(input)));
|
|
26
39
|
}
|
|
27
40
|
/**
|
|
28
41
|
* The dials over the findings a record could name, each read against the sweep that
|
|
@@ -47,10 +60,11 @@ function dialsOver(input, found, suppressed, unmarked) {
|
|
|
47
60
|
*/
|
|
48
61
|
export function decidedFindings(input, suppressed = { unmarked: input, records: [] }) {
|
|
49
62
|
const found = completedOver(input);
|
|
50
|
-
const unmarked = suppressed.records.length === 0 ? [] : completedOver(suppressed.unmarked);
|
|
63
|
+
const unmarked = suppressed.records.length === 0 ? [] : completedOver(suppressed.unmarked, fallenMembers);
|
|
51
64
|
const would = [...unmarked, ...found];
|
|
52
65
|
const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
|
|
53
|
-
const
|
|
66
|
+
const unanswered = heldRefs(input);
|
|
67
|
+
const kept = reportable(input, found.filter((finding) => !ledger.withheld(finding) && !unanswered.has(finding.symbol.ref)));
|
|
54
68
|
const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
|
|
55
69
|
return {
|
|
56
70
|
findings: evaluated.findings,
|
|
@@ -88,8 +88,14 @@ function unusedMessage(candidate) {
|
|
|
88
88
|
? UNUSED_TEST_MESSAGE
|
|
89
89
|
: UNUSED_MESSAGE;
|
|
90
90
|
}
|
|
91
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* Whether one interface method has implementations and every one has an empty body, or
|
|
93
|
+
* is one whose implementations the run could not read in full.
|
|
94
|
+
*/
|
|
92
95
|
function marker(input, method) {
|
|
96
|
+
if (input.implementations.unknown.has(method)) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
93
99
|
const bodies = [...(input.implementations.bodies.get(method)?.values() ?? [])];
|
|
94
100
|
return bodies.length > 0 && bodies.every((empty) => empty);
|
|
95
101
|
}
|