@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
|
@@ -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
|
}
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { isCallExpression, isElementAccessExpression, isIdentifier, isNoSubstitutionTemplateLiteral, isPropertyAccessExpression, isStringLiteral, } from "@typescript/native/unstable/ast";
|
|
16
16
|
import { TypeFlags } from "@typescript/native/unstable/sync";
|
|
17
|
-
import { typesAt } from "./class-members.js";
|
|
18
17
|
import { memberNames } from "./member-names.js";
|
|
19
18
|
import { renderPosition } from "./position.js";
|
|
19
|
+
import { isAnswered, UNANSWERED } from "./query.js";
|
|
20
20
|
/** The `Reflect` methods whose second argument is a property key. */
|
|
21
21
|
const REFLECT_KEYED = new Set(["get", "set", "has"]);
|
|
22
22
|
/** The text one key denotes, where it is literal text. */
|
|
@@ -67,14 +67,17 @@ function lookupsOf(file) {
|
|
|
67
67
|
file.forEachChild(visit);
|
|
68
68
|
return found;
|
|
69
69
|
}
|
|
70
|
-
/**
|
|
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
|
+
*/
|
|
71
75
|
function readsAMapping(project, type) {
|
|
72
76
|
if ((type.flags & TypeFlags.Any) !== 0) {
|
|
73
77
|
return false;
|
|
74
78
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
.some((info) => (info.keyType.flags & TypeFlags.String) !== 0);
|
|
79
|
+
const infos = project.queries.indexInfosOf(type);
|
|
80
|
+
return isAnswered(infos) && infos.some((info) => (info.keyType.flags & TypeFlags.String) !== 0);
|
|
78
81
|
}
|
|
79
82
|
/**
|
|
80
83
|
* The reflective-lookup detector: every member whose name a literal key of a lookup
|
|
@@ -89,13 +92,16 @@ export function reflectiveLookup(input) {
|
|
|
89
92
|
}
|
|
90
93
|
// Whether each written `Reflect` is the global one, asked in one batch: a
|
|
91
94
|
// declaration of that name in one of the project's own files shadows it.
|
|
92
|
-
const ownFiles =
|
|
95
|
+
const ownFiles = project.ownPaths();
|
|
93
96
|
const reflects = lookups.flatMap((lookup) => lookup.reflect === undefined ? [] : [lookup.reflect]);
|
|
94
97
|
const global = new Set();
|
|
95
98
|
if (reflects.length > 0) {
|
|
96
99
|
project.symbolsAt(reflects.map((node) => project.handle(node))).forEach((symbol, index) => {
|
|
97
100
|
const node = reflects[index];
|
|
98
|
-
|
|
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));
|
|
99
105
|
if (node !== undefined && declaredHere === false) {
|
|
100
106
|
global.add(node);
|
|
101
107
|
}
|
|
@@ -105,9 +111,12 @@ export function reflectiveLookup(input) {
|
|
|
105
111
|
// reads that mapping rather than a member, asked in one batch.
|
|
106
112
|
const receivers = lookups.flatMap((lookup) => lookup.receiver === undefined ? [] : [lookup.receiver]);
|
|
107
113
|
const mapped = new Set();
|
|
108
|
-
typesAt(
|
|
114
|
+
project.queries.typesAt(receivers).forEach((type, index) => {
|
|
109
115
|
const receiver = receivers[index];
|
|
110
|
-
if (receiver !== undefined &&
|
|
116
|
+
if (receiver !== undefined &&
|
|
117
|
+
type !== undefined &&
|
|
118
|
+
isAnswered(type) &&
|
|
119
|
+
readsAMapping(project, type)) {
|
|
111
120
|
mapped.add(receiver);
|
|
112
121
|
}
|
|
113
122
|
});
|
package/dist/src/report.js
CHANGED
|
@@ -218,7 +218,9 @@ export function buildReport(input) {
|
|
|
218
218
|
configurations: [...input.configurations]
|
|
219
219
|
.sort((a, b) => compare(a.id, b.id))
|
|
220
220
|
.map((one) => ({ id: one.id, project: one.project })),
|
|
221
|
-
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 })),
|
|
222
224
|
consumers: { declared: loaded.length + unavailable.length, loaded, unavailable },
|
|
223
225
|
findings,
|
|
224
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);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { jsonChunks } from "../json-chunks.js";
|
|
1
2
|
import { KINDS } from "../kinds.js";
|
|
2
3
|
import { lineHashes, symbolFingerprint } from "./fingerprint.js";
|
|
3
4
|
import { RenderError } from "./reporter.js";
|
|
@@ -236,5 +237,5 @@ export function sarif(report, options) {
|
|
|
236
237
|
},
|
|
237
238
|
],
|
|
238
239
|
};
|
|
239
|
-
return
|
|
240
|
+
return jsonChunks(log, INDENT);
|
|
240
241
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* and `println`. Any other function, `define`, `template` and `block` are refused at parse,
|
|
7
7
|
* and a member the document does not carry fails the rendering.
|
|
8
8
|
*/
|
|
9
|
+
import { CHUNK_LENGTH } from "../json-chunks.js";
|
|
9
10
|
import { RenderError } from "./reporter.js";
|
|
10
11
|
import { utf8Bytes } from "./sha256.js";
|
|
11
12
|
/** A template that does not parse, which refuses the invocation before any analysis. */
|
|
@@ -735,10 +736,22 @@ function call(name, args) {
|
|
|
735
736
|
/** One execution of a template over a document. */
|
|
736
737
|
class Execution {
|
|
737
738
|
scopes;
|
|
738
|
-
|
|
739
|
+
/** The rendering so far, in pieces of about {@link CHUNK_LENGTH}, the last still growing. */
|
|
740
|
+
pieces = [""];
|
|
739
741
|
constructor(root) {
|
|
740
742
|
this.scopes = [new Map([["$", root]])];
|
|
741
743
|
}
|
|
744
|
+
write(text) {
|
|
745
|
+
const last = this.pieces.length - 1;
|
|
746
|
+
const held = (this.pieces[last] ?? "") + text;
|
|
747
|
+
if (held.length >= CHUNK_LENGTH) {
|
|
748
|
+
this.pieces[last] = held;
|
|
749
|
+
this.pieces.push("");
|
|
750
|
+
}
|
|
751
|
+
else {
|
|
752
|
+
this.pieces[last] = held;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
742
755
|
lookup(name) {
|
|
743
756
|
for (let i = this.scopes.length - 1; i >= 0; i -= 1) {
|
|
744
757
|
const scope = this.scopes[i];
|
|
@@ -762,7 +775,7 @@ class Execution {
|
|
|
762
775
|
let held = value;
|
|
763
776
|
for (const name of names) {
|
|
764
777
|
if (!isObject(held)) {
|
|
765
|
-
throw new RenderError(`
|
|
778
|
+
throw new RenderError(`cannot evaluate field ${name} in a ${kindOf(held)}`);
|
|
766
779
|
}
|
|
767
780
|
if (!Object.hasOwn(held, name)) {
|
|
768
781
|
throw new RenderError(`the document has no member ${JSON.stringify(name)} here`);
|
|
@@ -794,7 +807,7 @@ class Execution {
|
|
|
794
807
|
}
|
|
795
808
|
if (head.k !== "func") {
|
|
796
809
|
if (rest.length > 0 || piped !== undefined) {
|
|
797
|
-
throw new RenderError("
|
|
810
|
+
throw new RenderError("cannot give an argument to a non-function");
|
|
798
811
|
}
|
|
799
812
|
return this.arg(head, dot);
|
|
800
813
|
}
|
|
@@ -842,12 +855,12 @@ class Execution {
|
|
|
842
855
|
for (const node of nodes) {
|
|
843
856
|
switch (node.k) {
|
|
844
857
|
case "text":
|
|
845
|
-
this.
|
|
858
|
+
this.write(node.text);
|
|
846
859
|
break;
|
|
847
860
|
case "action": {
|
|
848
861
|
const value = this.pipeline(node.pipe, dot, true);
|
|
849
862
|
if (node.pipe.vars.length === 0) {
|
|
850
|
-
this.
|
|
863
|
+
this.write(formatted(value, false));
|
|
851
864
|
}
|
|
852
865
|
break;
|
|
853
866
|
}
|
|
@@ -890,7 +903,7 @@ class Execution {
|
|
|
890
903
|
.map((key) => [key, value[key] ?? null]);
|
|
891
904
|
}
|
|
892
905
|
else {
|
|
893
|
-
throw new RenderError(`range
|
|
906
|
+
throw new RenderError(`range cannot iterate over a ${kindOf(value)}`);
|
|
894
907
|
}
|
|
895
908
|
if (entries.length === 0) {
|
|
896
909
|
this.run(node.otherwise, dot);
|
|
@@ -925,12 +938,40 @@ class Execution {
|
|
|
925
938
|
}
|
|
926
939
|
}
|
|
927
940
|
/**
|
|
928
|
-
*
|
|
929
|
-
*
|
|
941
|
+
* One document as the JSON value it encodes: what `JSON.stringify` would write for it,
|
|
942
|
+
* read back, without writing it. A member holding nothing is left out, and an array
|
|
943
|
+
* element holding nothing, like a number that is not finite, is null.
|
|
944
|
+
*/
|
|
945
|
+
function plainOf(value) {
|
|
946
|
+
if (Array.isArray(value)) {
|
|
947
|
+
return value.map((element) => plainOf(element) ?? null);
|
|
948
|
+
}
|
|
949
|
+
if (typeof value === "object" && value !== null) {
|
|
950
|
+
const held = {};
|
|
951
|
+
for (const [key, member] of Object.entries(value)) {
|
|
952
|
+
const plain = plainOf(member);
|
|
953
|
+
if (plain !== undefined) {
|
|
954
|
+
held[key] = plain;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return held;
|
|
958
|
+
}
|
|
959
|
+
if (typeof value === "number") {
|
|
960
|
+
return Number.isFinite(value) ? value : null;
|
|
961
|
+
}
|
|
962
|
+
if (typeof value === "string" || typeof value === "boolean" || value === null) {
|
|
963
|
+
return value;
|
|
964
|
+
}
|
|
965
|
+
return undefined;
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* The template's rendering of one document, in pieces of about {@link CHUNK_LENGTH}.
|
|
969
|
+
* The rendering is complete before it is answered, so a template that fails partway
|
|
970
|
+
* renders nothing, and the failure names what it could not evaluate.
|
|
930
971
|
*/
|
|
931
972
|
export function renderTemplate(template, document) {
|
|
932
|
-
const root =
|
|
973
|
+
const root = plainOf(document) ?? null;
|
|
933
974
|
const execution = new Execution(root);
|
|
934
975
|
execution.run(template.nodes, root);
|
|
935
|
-
return execution.
|
|
976
|
+
return execution.pieces.filter((piece) => piece !== "");
|
|
936
977
|
}
|