@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
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one way the analysis asks a project's checker anything. A question the compiler
|
|
3
|
+
* server panics on answers {@link UNANSWERED}, and a batch it panics on whole is asked
|
|
4
|
+
* again one question at a time; every caller reads an unanswered question as keeping a
|
|
5
|
+
* declaration live, and each is recorded once so a run can say where it was partial.
|
|
6
|
+
* Questions of one accessor about many locations go out together, through the array
|
|
7
|
+
* form where it has one and otherwise as one batch, one round trip per capped run.
|
|
8
|
+
*/
|
|
9
|
+
import { isIdentifier } from "@typescript/native/unstable/ast";
|
|
10
|
+
/**
|
|
11
|
+
* The greatest number of locations one batched request asks about. A batch is
|
|
12
|
+
* answered by an array of its own length, so an uncapped batch over a large file
|
|
13
|
+
* makes one answer proportional to that file. The figure bounds the size of one
|
|
14
|
+
* answer rather than the work: the median file is one batch at this cap and the
|
|
15
|
+
* largest production file a handful.
|
|
16
|
+
*/
|
|
17
|
+
export const DEFAULT_BATCH_CAP = 4096;
|
|
18
|
+
/** The answer to a question the checker could not answer. */
|
|
19
|
+
export const UNANSWERED = Symbol("unanswered");
|
|
20
|
+
/** Whether one answer is an answer. */
|
|
21
|
+
export function isAnswered(answer) {
|
|
22
|
+
return answer !== UNANSWERED;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The stop a pass raises where it cannot go on without an answer the checker did not
|
|
26
|
+
* give. Whoever runs the pass catches it and keeps live everything the pass could
|
|
27
|
+
* have kept live, so a pass that throws it never computes a finding from the gap.
|
|
28
|
+
*/
|
|
29
|
+
export class Unanswerable extends Error {
|
|
30
|
+
constructor() {
|
|
31
|
+
super("the checker did not answer a question this pass needs");
|
|
32
|
+
this.name = "Unanswerable";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** The answer itself, or the stop of a pass that cannot go on without it. */
|
|
36
|
+
export function must(answer) {
|
|
37
|
+
if (answer === UNANSWERED) {
|
|
38
|
+
throw new Unanswerable();
|
|
39
|
+
}
|
|
40
|
+
return answer;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Whether one failure is the server's report that a question panicked. The server
|
|
44
|
+
* recovers the panic and answers the question with its text, so the session
|
|
45
|
+
* survives it; any other failure, a closed channel among them, is not one this layer
|
|
46
|
+
* contains.
|
|
47
|
+
*/
|
|
48
|
+
function panicked(error) {
|
|
49
|
+
return error instanceof Error && error.message.startsWith("panic:");
|
|
50
|
+
}
|
|
51
|
+
const ASK_DIRECTLY = (_accessor, _locations, question) => question();
|
|
52
|
+
/** What one run could not ask, each question once. */
|
|
53
|
+
export class UnansweredLog {
|
|
54
|
+
#seen = new Map();
|
|
55
|
+
/** Records one question, once however many times it is asked. */
|
|
56
|
+
note(question) {
|
|
57
|
+
const key = [question.configFile, question.accessor, question.location].join("\u0000");
|
|
58
|
+
if (!this.#seen.has(key)) {
|
|
59
|
+
this.#seen.set(key, question);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Every question recorded, in the order first asked. */
|
|
63
|
+
get questions() {
|
|
64
|
+
return [...this.#seen.values()];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function nodeLocation(node) {
|
|
68
|
+
return `${node.getSourceFile().fileName}:${String(node.pos)}`;
|
|
69
|
+
}
|
|
70
|
+
function symbolLocation(symbol) {
|
|
71
|
+
return `symbol ${String(symbol.id)} ${symbol.name}`;
|
|
72
|
+
}
|
|
73
|
+
function typeLocation(type) {
|
|
74
|
+
return `type ${String(type.id)}`;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The guarded questions over one project's checker. `configFile` names the project
|
|
78
|
+
* in the log, `batch` sends many questions in one round trip, `log` is the run's,
|
|
79
|
+
* shared by every project, and every question is put through `ask`.
|
|
80
|
+
*/
|
|
81
|
+
export function queriesOf(checker, configFile, batch, log, ask = ASK_DIRECTLY) {
|
|
82
|
+
const note = (accessor, location) => {
|
|
83
|
+
log.note({ configFile, accessor, location });
|
|
84
|
+
};
|
|
85
|
+
const one = (accessor, location, question) => {
|
|
86
|
+
try {
|
|
87
|
+
return ask(accessor, () => [location()], question);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (!panicked(error)) {
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
note(accessor, location());
|
|
94
|
+
return UNANSWERED;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* One question per item of `items`, in runs of at most `cap`, each run in one
|
|
99
|
+
* round trip. A run fails as a whole only where the server panics outside any one
|
|
100
|
+
* question, and is then asked one question at a time.
|
|
101
|
+
*/
|
|
102
|
+
const each = (accessor, items, cap, locate, question, alone) => {
|
|
103
|
+
function* guarded(item) {
|
|
104
|
+
try {
|
|
105
|
+
return yield* ask(accessor, () => [locate(item)], () => question(item));
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (!panicked(error)) {
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
note(accessor, locate(item));
|
|
112
|
+
return UNANSWERED;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const answers = [];
|
|
116
|
+
for (let from = 0; from < items.length; from += cap) {
|
|
117
|
+
const run = items.slice(from, from + cap);
|
|
118
|
+
try {
|
|
119
|
+
answers.push(...batch(run.map(guarded)));
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (!panicked(error)) {
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
answers.push(...run.map((item) => one(accessor, () => locate(item), () => alone(item))));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return answers;
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* One accessor's array form over `items`, in runs of at most `cap`. A run the server
|
|
132
|
+
* panics on is asked again as two halves, down to one item, so the items it can
|
|
133
|
+
* answer are answered and only an item that panics alone is unanswered.
|
|
134
|
+
*/
|
|
135
|
+
const many = (accessor, items, cap, locate, question) => {
|
|
136
|
+
const answers = [];
|
|
137
|
+
const settle = (run) => {
|
|
138
|
+
try {
|
|
139
|
+
answers.push(...ask(accessor, () => run.map(locate), () => question(run)));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (!panicked(error)) {
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const [only] = run;
|
|
148
|
+
if (run.length === 1 && only !== undefined) {
|
|
149
|
+
note(accessor, locate(only));
|
|
150
|
+
answers.push(UNANSWERED);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const half = Math.ceil(run.length / 2);
|
|
154
|
+
settle(run.slice(0, half));
|
|
155
|
+
settle(run.slice(half));
|
|
156
|
+
};
|
|
157
|
+
for (let from = 0; from < items.length; from += cap) {
|
|
158
|
+
settle(items.slice(from, from + cap));
|
|
159
|
+
}
|
|
160
|
+
return answers;
|
|
161
|
+
};
|
|
162
|
+
return {
|
|
163
|
+
symbolsAt: (nodes, cap = DEFAULT_BATCH_CAP) => many("getSymbolAtLocation", nodes, cap, nodeLocation, (run) => checker.getSymbolAtLocation(run)),
|
|
164
|
+
resolvedSymbol: (node) => isIdentifier(node)
|
|
165
|
+
? one("getResolvedSymbol", () => nodeLocation(node), () => checker.getResolvedSymbol(node))
|
|
166
|
+
: undefined,
|
|
167
|
+
shorthandValue: (node) => one("getShorthandAssignmentValueSymbol", () => nodeLocation(node), () => checker.getShorthandAssignmentValueSymbol(node)),
|
|
168
|
+
immediateAliased: (symbol) => one("getImmediateAliasedSymbol", () => symbolLocation(symbol), () => checker.getImmediateAliasedSymbol(symbol)),
|
|
169
|
+
aliased: (symbol) => one("getAliasedSymbol", () => symbolLocation(symbol), () => checker.getAliasedSymbol(symbol)),
|
|
170
|
+
resolveName: (name, meaning, location, excludeGlobals) => one("resolveName", () => (location === undefined ? `name ${name}` : nodeLocation(location)), () => checker.resolveName(name, meaning, location, excludeGlobals)),
|
|
171
|
+
exportsOfModule: (module) => one("getExportsOfModule", () => symbolLocation(module), () => checker.getExportsOfModule(module)),
|
|
172
|
+
membersOf: (symbol) => one("getMembers", () => symbolLocation(symbol), () => symbol.getMembers()),
|
|
173
|
+
exportsOf: (symbol) => one("getExports", () => symbolLocation(symbol), () => symbol.getExports()),
|
|
174
|
+
parentOf: (symbol) => one("getParent", () => symbolLocation(symbol), () => symbol.getParent()),
|
|
175
|
+
jsDocTags: (symbols, cap = DEFAULT_BATCH_CAP) => each("getJsDocTags", symbols, cap, symbolLocation, (symbol) => symbol.getJsDocTags.gen(checker), (symbol) => symbol.getJsDocTags(checker)),
|
|
176
|
+
typesAt: (nodes, cap = DEFAULT_BATCH_CAP) => many("getTypeAtLocation", nodes, cap, nodeLocation, (run) => checker.getTypeAtLocation(run)),
|
|
177
|
+
typeAt: (node) => one("getTypeAtLocation", () => nodeLocation(node), () => checker.getTypeAtLocation(node)),
|
|
178
|
+
typesOfSymbols: (symbols, cap = DEFAULT_BATCH_CAP) => many("getTypeOfSymbol", symbols, cap, symbolLocation, (run) => checker.getTypeOfSymbol(run)),
|
|
179
|
+
typeOfSymbol: (symbol) => one("getTypeOfSymbol", () => symbolLocation(symbol), () => checker.getTypeOfSymbol(symbol)),
|
|
180
|
+
declaredTypeOf: (symbol) => one("getDeclaredTypeOfSymbol", () => symbolLocation(symbol), () => checker.getDeclaredTypeOfSymbol(symbol)),
|
|
181
|
+
contextualTypes: (nodes, cap = DEFAULT_BATCH_CAP) => each("getContextualType", nodes, cap, nodeLocation, (node) => checker.getContextualType.gen(node), (node) => checker.getContextualType(node)),
|
|
182
|
+
propertiesOf: (type) => one("getPropertiesOfType", () => typeLocation(type), () => checker.getPropertiesOfType(type)),
|
|
183
|
+
indexInfosOf: (type) => one("getIndexInfosOfType", () => typeLocation(type), () => checker.getIndexInfosOfType(type)),
|
|
184
|
+
typeArguments: (type) => type.isTypeReference()
|
|
185
|
+
? one("getTypeArguments", () => typeLocation(type), () => checker.getTypeArguments(type))
|
|
186
|
+
: [],
|
|
187
|
+
nonNullable: (type) => one("getNonNullableType", () => typeLocation(type), () => checker.getNonNullableType(type)),
|
|
188
|
+
isArray: (type) => one("isArrayType", () => typeLocation(type), () => checker.isArrayType(type)),
|
|
189
|
+
assignableEach: (pairs, cap = DEFAULT_BATCH_CAP) => each("isTypeAssignableTo", pairs, cap, (pair) => `${typeLocation(pair.source)} to ${typeLocation(pair.target)}`, (pair) => checker.isTypeAssignableTo.gen(pair.source, pair.target), (pair) => checker.isTypeAssignableTo(pair.source, pair.target)),
|
|
190
|
+
baseTypes: (type) => type.isClassOrInterface()
|
|
191
|
+
? one("getBaseTypes", () => typeLocation(type), () => checker.getBaseTypes(type))
|
|
192
|
+
: [],
|
|
193
|
+
constituents: (type) => type.isUnionType() || type.isIntersectionType()
|
|
194
|
+
? one("getTypes", () => typeLocation(type), () => type.getTypes())
|
|
195
|
+
: [type],
|
|
196
|
+
targetOf: (type) => type.isTypeReference()
|
|
197
|
+
? one("getTarget", () => typeLocation(type), () => type.getTarget())
|
|
198
|
+
: type,
|
|
199
|
+
symbolOfType: (type) => one("getSymbol", () => typeLocation(type), () => type.getSymbol()),
|
|
200
|
+
signaturesOf: (type, kind) => one("getSignaturesOfType", () => typeLocation(type), () => checker.getSignaturesOfType(type, kind)),
|
|
201
|
+
resolvedSignature: (node) => one("getResolvedSignature", () => nodeLocation(node), () => checker.getResolvedSignature(node)),
|
|
202
|
+
parameterType: (signature, index) => one("getParameterType", () => `signature ${String(signature.id)} parameter ${String(index)}`, () => checker.getParameterType(signature, index)),
|
|
203
|
+
signatureOf: (declaration) => one("getSignatureFromDeclaration", () => nodeLocation(declaration), () => checker.getSignatureFromDeclaration(declaration)),
|
|
204
|
+
returnTypeOf: (signature) => one("getReturnTypeOfSignature", () => `signature ${String(signature.id)}`, () => checker.getReturnTypeOfSignature(signature)),
|
|
205
|
+
typeToString: (type) => one("typeToString", () => typeLocation(type), () => checker.typeToString(type)),
|
|
206
|
+
};
|
|
207
|
+
}
|
package/dist/src/references.js
CHANGED
|
@@ -2,15 +2,10 @@ import { isArrayLiteralExpression, isBinaryExpression, isCallExpression, isDecor
|
|
|
2
2
|
import { aliasChains } from "./alias-chain.js";
|
|
3
3
|
import { destructuring, propertyReadsOf } from "./destructuring.js";
|
|
4
4
|
import { globExpression } from "./glob.js";
|
|
5
|
-
import { nodeKey } from "./inventory.js";
|
|
5
|
+
import { declarationsByName, nodeKey } from "./inventory.js";
|
|
6
6
|
import { relativePath } from "./paths.js";
|
|
7
7
|
import { byPosition, renderPosition } from "./position.js";
|
|
8
|
-
|
|
9
|
-
* The batch cap a run takes when the configuration names none. It bounds the size of
|
|
10
|
-
* one answer rather than the work: the median file is one batch at this cap and the
|
|
11
|
-
* largest production file a handful.
|
|
12
|
-
*/
|
|
13
|
-
export const DEFAULT_BATCH_CAP = 4096;
|
|
8
|
+
import { DEFAULT_BATCH_CAP, UNANSWERED } from "./query.js";
|
|
14
9
|
/**
|
|
15
10
|
* The operators whose left side one assignment stores into. A compound assignment is
|
|
16
11
|
* among them: it reads its target only to write the result back, so nothing else
|
|
@@ -294,6 +289,49 @@ export function testFileRulesOf(testFiles, paths) {
|
|
|
294
289
|
})
|
|
295
290
|
.sort((a, b) => compare(a.rule, b.rule));
|
|
296
291
|
}
|
|
292
|
+
const importedCache = new WeakMap();
|
|
293
|
+
/**
|
|
294
|
+
* Each local name one file's imports bind, mapped to the names it imports: the
|
|
295
|
+
* exported name of a named import, `default` for a default import, and `undefined` for
|
|
296
|
+
* a namespace import or an import assignment, which binds a whole module.
|
|
297
|
+
*/
|
|
298
|
+
function importedNames(file) {
|
|
299
|
+
const known = importedCache.get(file);
|
|
300
|
+
if (known !== undefined) {
|
|
301
|
+
return known;
|
|
302
|
+
}
|
|
303
|
+
const names = new Map();
|
|
304
|
+
const bind = (local, imported) => {
|
|
305
|
+
names.set(local, [...(names.get(local) ?? []), imported]);
|
|
306
|
+
};
|
|
307
|
+
for (const statement of file.statements) {
|
|
308
|
+
if (isImportEqualsDeclaration(statement)) {
|
|
309
|
+
bind(statement.name.text, undefined);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const clause = isImportDeclaration(statement) ? statement.importClause : undefined;
|
|
313
|
+
if (clause === undefined) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (clause.name !== undefined) {
|
|
317
|
+
bind(clause.name.text, "default");
|
|
318
|
+
}
|
|
319
|
+
const bindings = clause.namedBindings;
|
|
320
|
+
if (bindings === undefined) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (bindings.kind === SyntaxKind.NamespaceImport) {
|
|
324
|
+
bind(bindings.name.text, undefined);
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
for (const element of bindings.elements) {
|
|
328
|
+
bind(element.name.text, element.propertyName?.text ?? element.name.text);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
importedCache.set(file, names);
|
|
333
|
+
return names;
|
|
334
|
+
}
|
|
297
335
|
/** Two strings ordered bytewise, which is the order every set of a run is read in. */
|
|
298
336
|
function compare(a, b) {
|
|
299
337
|
if (a === b) {
|
|
@@ -347,7 +385,26 @@ export function references(project, held, targetRoot, options) {
|
|
|
347
385
|
* Each later link is a declaration of its own that references the next, so one link
|
|
348
386
|
* per re-export is what lets a chain be followed one declaration at a time.
|
|
349
387
|
*/
|
|
350
|
-
const carriedBy = (link, symbol) => link.alias
|
|
388
|
+
const carriedBy = (link, symbol) => link.alias
|
|
389
|
+
? chains.nextDeclared(symbol)
|
|
390
|
+
: chains.declarationsOf(symbol).map((id) => ({ id, stepped: false, guessed: false }));
|
|
391
|
+
/**
|
|
392
|
+
* Every declaration bearing the name one node spells, or the name an import of its
|
|
393
|
+
* file binds that name to, which is what a reference the checker could not resolve is
|
|
394
|
+
* counted as: it may be a use of any of them, and a declaration it may use is kept
|
|
395
|
+
* live. A name a namespace import binds may be a use of any file.
|
|
396
|
+
*/
|
|
397
|
+
const named = (node) => {
|
|
398
|
+
const byName = declarationsByName(held);
|
|
399
|
+
const text = node.getText();
|
|
400
|
+
const imported = importedNames(node.getSourceFile()).get(text) ?? [];
|
|
401
|
+
return [
|
|
402
|
+
...new Set([
|
|
403
|
+
...(byName.get(text) ?? []),
|
|
404
|
+
...imported.flatMap((name) => name === undefined ? [...fileIds] : (byName.get(name) ?? [])),
|
|
405
|
+
]),
|
|
406
|
+
];
|
|
407
|
+
};
|
|
351
408
|
/** The symbol one site resolves to, asking only the accessor the site's form names. */
|
|
352
409
|
const symbolFor = (site, answered) => {
|
|
353
410
|
if (site.shorthand !== undefined) {
|
|
@@ -355,7 +412,7 @@ export function references(project, held, targetRoot, options) {
|
|
|
355
412
|
return project.shorthandValueAt(project.handle(site.shorthand));
|
|
356
413
|
}
|
|
357
414
|
const batched = answered.get(site.node);
|
|
358
|
-
if (batched
|
|
415
|
+
if (batched === UNANSWERED || typeof batched === "object") {
|
|
359
416
|
return batched;
|
|
360
417
|
}
|
|
361
418
|
residueFallbacks += 1;
|
|
@@ -397,7 +454,7 @@ export function references(project, held, targetRoot, options) {
|
|
|
397
454
|
batched += batching.length;
|
|
398
455
|
for (let from = 0; from < batching.length; from += cap) {
|
|
399
456
|
const run = batching.slice(from, from + cap);
|
|
400
|
-
const answers = project.symbolsAt(run.map((node) => project.handle(node)));
|
|
457
|
+
const answers = project.symbolsAt(run.map((node) => project.handle(node)), cap);
|
|
401
458
|
fileBatches += 1;
|
|
402
459
|
run.forEach((node, index) => {
|
|
403
460
|
const symbol = answers[index];
|
|
@@ -412,13 +469,16 @@ export function references(project, held, targetRoot, options) {
|
|
|
412
469
|
continue;
|
|
413
470
|
}
|
|
414
471
|
const position = renderPosition(file, root, link.at.getStart());
|
|
415
|
-
|
|
472
|
+
const carried = symbol === UNANSWERED
|
|
473
|
+
? named(link.at).map((id) => ({ id, stepped: false, guessed: true }))
|
|
474
|
+
: carriedBy(link, symbol);
|
|
475
|
+
for (const target of carried) {
|
|
416
476
|
found.push({
|
|
417
477
|
from: link.from,
|
|
418
|
-
to: id,
|
|
478
|
+
to: target.id,
|
|
419
479
|
position,
|
|
420
480
|
use: "read",
|
|
421
|
-
resolution: link.alias ? "alias" : "batch",
|
|
481
|
+
resolution: target.guessed ? "by-name" : link.alias ? "alias" : "batch",
|
|
422
482
|
test,
|
|
423
483
|
});
|
|
424
484
|
}
|
|
@@ -429,14 +489,16 @@ export function references(project, held, targetRoot, options) {
|
|
|
429
489
|
continue;
|
|
430
490
|
}
|
|
431
491
|
const position = renderPosition(file, root, evaluation.node.getStart());
|
|
432
|
-
|
|
492
|
+
// A module the checker could not resolve may be any file of the target.
|
|
493
|
+
const evaluated = module === UNANSWERED ? [...fileIds] : chains.declarationsOf(module);
|
|
494
|
+
for (const id of evaluated) {
|
|
433
495
|
if (fileIds.has(id)) {
|
|
434
496
|
found.push({
|
|
435
497
|
from: evaluation.from,
|
|
436
498
|
to: id,
|
|
437
499
|
position,
|
|
438
500
|
use: "evaluation",
|
|
439
|
-
resolution: "batch",
|
|
501
|
+
resolution: module === UNANSWERED ? "by-name" : "batch",
|
|
440
502
|
test,
|
|
441
503
|
});
|
|
442
504
|
}
|
|
@@ -450,13 +512,20 @@ export function references(project, held, targetRoot, options) {
|
|
|
450
512
|
return;
|
|
451
513
|
}
|
|
452
514
|
const position = renderPosition(file, root, read.key.getStart());
|
|
453
|
-
|
|
515
|
+
const targets = symbol === UNANSWERED
|
|
516
|
+
? (declarationsByName(held).get(read.name) ?? []).map((id) => ({
|
|
517
|
+
id,
|
|
518
|
+
stepped: false,
|
|
519
|
+
guessed: true,
|
|
520
|
+
}))
|
|
521
|
+
: chains.chainOf(symbol);
|
|
522
|
+
for (const target of targets) {
|
|
454
523
|
found.push({
|
|
455
524
|
from: read.from,
|
|
456
525
|
to: target.id,
|
|
457
526
|
position,
|
|
458
527
|
use: "read",
|
|
459
|
-
resolution: "destructured",
|
|
528
|
+
resolution: target.guessed ? "by-name" : "destructured",
|
|
460
529
|
test,
|
|
461
530
|
});
|
|
462
531
|
}
|
|
@@ -464,7 +533,7 @@ export function references(project, held, targetRoot, options) {
|
|
|
464
533
|
for (const site of uses) {
|
|
465
534
|
const direct = site.shorthand !== undefined
|
|
466
535
|
? "shorthand"
|
|
467
|
-
: answered.
|
|
536
|
+
: typeof answered.get(site.node) === "object"
|
|
468
537
|
? "batch"
|
|
469
538
|
: "resolved-symbol";
|
|
470
539
|
const symbol = symbolFor(site, answered);
|
|
@@ -474,13 +543,16 @@ export function references(project, held, targetRoot, options) {
|
|
|
474
543
|
const position = renderPosition(file, root, site.node.getStart());
|
|
475
544
|
// A re-export something imports is used and so is the declaration behind it, so
|
|
476
545
|
// the use names every link of the chain its name stands at the head of.
|
|
477
|
-
|
|
546
|
+
const targets = symbol === UNANSWERED
|
|
547
|
+
? named(site.node).map((id) => ({ id, stepped: false, guessed: true }))
|
|
548
|
+
: chains.chainOf(symbol);
|
|
549
|
+
for (const target of targets) {
|
|
478
550
|
found.push({
|
|
479
551
|
from: site.from,
|
|
480
552
|
to: target.id,
|
|
481
553
|
position,
|
|
482
554
|
use: site.use,
|
|
483
|
-
resolution: target.stepped ? "alias" : direct,
|
|
555
|
+
resolution: target.guessed ? "by-name" : target.stepped ? "alias" : direct,
|
|
484
556
|
test,
|
|
485
557
|
});
|
|
486
558
|
}
|
|
@@ -8,11 +8,15 @@
|
|
|
8
8
|
* string reaches every member of that name on any type, static members included,
|
|
9
9
|
* and every record names the string's own position. A key that is not literal text
|
|
10
10
|
* holds nothing back: a computed name is the case no exact rule serves. `Reflect` is
|
|
11
|
-
* the global one only: a local declaration of that name is a different object.
|
|
11
|
+
* the global one only: a local declaration of that name is a different object. An
|
|
12
|
+
* element access through a receiver whose type declares a string index signature, and is
|
|
13
|
+
* not `any`, reads that signature and holds nothing back.
|
|
12
14
|
*/
|
|
13
15
|
import { isCallExpression, isElementAccessExpression, isIdentifier, isNoSubstitutionTemplateLiteral, isPropertyAccessExpression, isStringLiteral, } from "@typescript/native/unstable/ast";
|
|
16
|
+
import { TypeFlags } from "@typescript/native/unstable/sync";
|
|
14
17
|
import { memberNames } from "./member-names.js";
|
|
15
18
|
import { renderPosition } from "./position.js";
|
|
19
|
+
import { isAnswered, UNANSWERED } from "./query.js";
|
|
16
20
|
/** The `Reflect` methods whose second argument is a property key. */
|
|
17
21
|
const REFLECT_KEYED = new Set(["get", "set", "has"]);
|
|
18
22
|
/** The text one key denotes, where it is literal text. */
|
|
@@ -34,6 +38,7 @@ function lookupsOf(file) {
|
|
|
34
38
|
name,
|
|
35
39
|
callee: `[${JSON.stringify(name)}]`,
|
|
36
40
|
reflect: undefined,
|
|
41
|
+
receiver: node.expression,
|
|
37
42
|
});
|
|
38
43
|
}
|
|
39
44
|
}
|
|
@@ -48,7 +53,13 @@ function lookupsOf(file) {
|
|
|
48
53
|
REFLECT_KEYED.has(method.text) &&
|
|
49
54
|
key !== undefined &&
|
|
50
55
|
name !== undefined) {
|
|
51
|
-
found.push({
|
|
56
|
+
found.push({
|
|
57
|
+
key,
|
|
58
|
+
name,
|
|
59
|
+
callee: `Reflect.${method.text}`,
|
|
60
|
+
reflect: target,
|
|
61
|
+
receiver: undefined,
|
|
62
|
+
});
|
|
52
63
|
}
|
|
53
64
|
}
|
|
54
65
|
node.forEachChild(visit);
|
|
@@ -56,6 +67,18 @@ function lookupsOf(file) {
|
|
|
56
67
|
file.forEachChild(visit);
|
|
57
68
|
return found;
|
|
58
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Whether a receiver's type is not `any` and declares a string index signature. A type
|
|
72
|
+
* whose index signatures the checker did not answer for is read as declaring none, so
|
|
73
|
+
* the lookup retains what it names.
|
|
74
|
+
*/
|
|
75
|
+
function readsAMapping(project, type) {
|
|
76
|
+
if ((type.flags & TypeFlags.Any) !== 0) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
const infos = project.queries.indexInfosOf(type);
|
|
80
|
+
return isAnswered(infos) && infos.some((info) => (info.keyType.flags & TypeFlags.String) !== 0);
|
|
81
|
+
}
|
|
59
82
|
/**
|
|
60
83
|
* The reflective-lookup detector: every member whose name a literal key of a lookup
|
|
61
84
|
* spells, recorded at the key.
|
|
@@ -69,24 +92,43 @@ export function reflectiveLookup(input) {
|
|
|
69
92
|
}
|
|
70
93
|
// Whether each written `Reflect` is the global one, asked in one batch: a
|
|
71
94
|
// declaration of that name in one of the project's own files shadows it.
|
|
72
|
-
const ownFiles =
|
|
95
|
+
const ownFiles = project.ownPaths();
|
|
73
96
|
const reflects = lookups.flatMap((lookup) => lookup.reflect === undefined ? [] : [lookup.reflect]);
|
|
74
97
|
const global = new Set();
|
|
75
98
|
if (reflects.length > 0) {
|
|
76
99
|
project.symbolsAt(reflects.map((node) => project.handle(node))).forEach((symbol, index) => {
|
|
77
100
|
const node = reflects[index];
|
|
78
|
-
|
|
101
|
+
// A name the checker did not answer for is read as the global, which retains.
|
|
102
|
+
const declaredHere = symbol === UNANSWERED
|
|
103
|
+
? false
|
|
104
|
+
: symbol?.declarations.some((declaration) => ownFiles.has(declaration.path));
|
|
79
105
|
if (node !== undefined && declaredHere === false) {
|
|
80
106
|
global.add(node);
|
|
81
107
|
}
|
|
82
108
|
});
|
|
83
109
|
}
|
|
110
|
+
// An element access through a receiver that maps every string key to one value type
|
|
111
|
+
// reads that mapping rather than a member, asked in one batch.
|
|
112
|
+
const receivers = lookups.flatMap((lookup) => lookup.receiver === undefined ? [] : [lookup.receiver]);
|
|
113
|
+
const mapped = new Set();
|
|
114
|
+
project.queries.typesAt(receivers).forEach((type, index) => {
|
|
115
|
+
const receiver = receivers[index];
|
|
116
|
+
if (receiver !== undefined &&
|
|
117
|
+
type !== undefined &&
|
|
118
|
+
isAnswered(type) &&
|
|
119
|
+
readsAMapping(project, type)) {
|
|
120
|
+
mapped.add(receiver);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
84
123
|
const members = memberNames(files, held);
|
|
85
124
|
const found = [];
|
|
86
125
|
for (const lookup of lookups) {
|
|
87
126
|
if (lookup.reflect !== undefined && !global.has(lookup.reflect)) {
|
|
88
127
|
continue;
|
|
89
128
|
}
|
|
129
|
+
if (lookup.receiver !== undefined && mapped.has(lookup.receiver)) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
90
132
|
const site = renderPosition(lookup.key.getSourceFile(), targetRoot, lookup.key.getStart());
|
|
91
133
|
for (const member of members.get(lookup.name) ?? []) {
|
|
92
134
|
found.push({ id: member.id, detail: `looked up by ${lookup.callee}`, site });
|
package/dist/src/report.js
CHANGED
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
* renders this document, so no two renderings disagree about which findings exist.
|
|
5
5
|
*/
|
|
6
6
|
/** The version of the report schema this analyzer writes a report to. */
|
|
7
|
-
|
|
7
|
+
const SCHEMA_VERSION = "6.1.0";
|
|
8
|
+
/**
|
|
9
|
+
* Every report schema version this analyzer reads: the schema versions the Contract it
|
|
10
|
+
* implements admits, each an instance of the one it writes.
|
|
11
|
+
*/
|
|
12
|
+
export const SCHEMA_VERSIONS_ACCEPTED = ["6.0.0", SCHEMA_VERSION];
|
|
8
13
|
/** The name this analyzer writes every document under and mints every identifier with. */
|
|
9
14
|
export const ANALYZER_NAME = "deadset-ts";
|
|
10
15
|
/** The language this analyzer claims. */
|
|
@@ -52,6 +57,7 @@ function wireDetails(details) {
|
|
|
52
57
|
...(entry.reason === undefined ? {} : { reason: entry.reason }),
|
|
53
58
|
},
|
|
54
59
|
}),
|
|
60
|
+
...(details.overlap === undefined ? {} : { overlap: details.overlap }),
|
|
55
61
|
};
|
|
56
62
|
}
|
|
57
63
|
/** One finding as the finding schema spells it, member for member in its order. */
|
|
@@ -79,6 +85,9 @@ export function wireFinding(finding) {
|
|
|
79
85
|
root: finding.component.root,
|
|
80
86
|
symbol_count: finding.component.symbolCount,
|
|
81
87
|
deletable_lines: finding.component.deletableLines,
|
|
88
|
+
...(finding.component.members === undefined
|
|
89
|
+
? {}
|
|
90
|
+
: { members: finding.component.members.map(wireSymbol) }),
|
|
82
91
|
},
|
|
83
92
|
retained_by: finding.retainedBy,
|
|
84
93
|
configurations: finding.configurations,
|
|
@@ -198,7 +207,7 @@ export function buildReport(input) {
|
|
|
198
207
|
name: ANALYZER_NAME,
|
|
199
208
|
version: input.version,
|
|
200
209
|
languages: [LANGUAGE],
|
|
201
|
-
schema_versions_accepted:
|
|
210
|
+
schema_versions_accepted: SCHEMA_VERSIONS_ACCEPTED,
|
|
202
211
|
conformance: {
|
|
203
212
|
corpus_version: input.conformance.corpusVersion,
|
|
204
213
|
result: input.conformance.result,
|
|
@@ -209,7 +218,9 @@ export function buildReport(input) {
|
|
|
209
218
|
configurations: [...input.configurations]
|
|
210
219
|
.sort((a, b) => compare(a.id, b.id))
|
|
211
220
|
.map((one) => ({ id: one.id, project: one.project })),
|
|
212
|
-
configurations_not_built: []
|
|
221
|
+
configurations_not_built: [...input.notBuilt]
|
|
222
|
+
.sort((a, b) => compare(a.id, b.id))
|
|
223
|
+
.map((one) => ({ id: one.id, project: one.project, error: one.error })),
|
|
213
224
|
consumers: { declared: loaded.length + unavailable.length, loaded, unavailable },
|
|
214
225
|
findings,
|
|
215
226
|
edge_evaluations: input.result.edgeEvaluations.map(wireEvaluation).sort(byEdge),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fails } from "../config.js";
|
|
2
|
+
import { coalesced } from "../json-chunks.js";
|
|
2
3
|
import { KINDS } from "../kinds.js";
|
|
3
4
|
/** The escapes a workflow command's message needs, so a line break does not end the command. */
|
|
4
5
|
const DATA_ESCAPES = { "%": "%25", "\r": "%0D", "\n": "%0A" };
|
|
@@ -28,9 +29,12 @@ function command(level, at, endLine, title, message) {
|
|
|
28
29
|
* because its kind is fixed on at the failing severity.
|
|
29
30
|
*/
|
|
30
31
|
export function annotations(report, options) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
return coalesced((function* lines() {
|
|
33
|
+
for (const found of report.findings) {
|
|
34
|
+
yield `${command(fails(found.severity, options.failOn) ? "error" : "warning", found.position, found.position.end_line, `${found.code} ${found.kind}`, found.message)}\n`;
|
|
35
|
+
}
|
|
36
|
+
for (const stale of report.stale_suppressions) {
|
|
37
|
+
yield `${command("error", stale.position, stale.position.line, `${stale.code} ${kindName(stale.code)}`, stale.message)}\n`;
|
|
38
|
+
}
|
|
39
|
+
})());
|
|
36
40
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
+
import { jsonChunks } from "../json-chunks.js";
|
|
1
2
|
/** The indentation of the report document, which with the schema's member order fixes its bytes. */
|
|
2
3
|
const INDENT = 2;
|
|
3
|
-
/** The report document itself: the report, indented, with a closing newline. */
|
|
4
|
-
export const json = (report) =>
|
|
4
|
+
/** The report document itself, in pieces: the report, indented, with a closing newline. */
|
|
5
|
+
export const json = (report) => jsonChunks(report, INDENT);
|