@cplieger/deadset-ts 3.13.0 → 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 +3 -3
- package/dist/bin/node-host.js +10 -7
- package/dist/src/alias-chain.js +59 -7
- package/dist/src/analysis.js +119 -23
- 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 +105 -28
- 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 +48 -7
- package/dist/src/reporters/text.js +8 -6
- package/dist/src/roots.js +27 -11
- 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 +40 -7
- 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 +223 -61
- 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 +177 -33
- 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 +50 -8
- package/src/reporters/text.ts +12 -12
- package/src/roots.ts +34 -12
- 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 +53 -7
- package/src/verbs/print-retained.ts +6 -2
- package/src/verbs/print-roots.ts +5 -1
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];
|
|
@@ -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
|
}
|
|
@@ -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
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { coalesced } from "../json-chunks.js";
|
|
1
2
|
/** The kind token and the confidence a stale suppression is rendered with, both fixed. */
|
|
2
3
|
const STALE_KIND = "suppression";
|
|
3
4
|
const STALE_CONFIDENCE = "certain";
|
|
@@ -24,11 +25,12 @@ function summary(totals) {
|
|
|
24
25
|
* summary is deliberately not in that shape, so a filter for finding lines yields exactly
|
|
25
26
|
* them. Nothing else is written: no timestamp, duration or host detail.
|
|
26
27
|
*/
|
|
27
|
-
export const text = (report) => {
|
|
28
|
-
const
|
|
28
|
+
export const text = (report) => coalesced((function* lines() {
|
|
29
|
+
for (const found of report.findings) {
|
|
30
|
+
yield `${found.position.path}:${String(found.position.line)}:${String(found.position.column)}: ${found.symbol.kind} ${found.symbol.name}: ${found.message} [${found.confidence}] (${found.code})\n`;
|
|
31
|
+
}
|
|
29
32
|
for (const stale of report.stale_suppressions) {
|
|
30
|
-
|
|
33
|
+
yield `${stale.position.path}:${String(stale.position.line)}:${String(stale.position.column)}: ${STALE_KIND} ${stale.symbol}: ${stale.message} [${STALE_CONFIDENCE}] (${stale.code})\n`;
|
|
31
34
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
};
|
|
35
|
+
yield `${summary(report.totals)}\n`;
|
|
36
|
+
})());
|
package/dist/src/roots.js
CHANGED
|
@@ -12,6 +12,7 @@ import { entryPoints } from "./entry-points.js";
|
|
|
12
12
|
import { globExpression } from "./glob.js";
|
|
13
13
|
import { nodeKey } from "./inventory.js";
|
|
14
14
|
import { byPosition } from "./position.js";
|
|
15
|
+
import { UNANSWERED } from "./query.js";
|
|
15
16
|
import { sourceFilesOf } from "./source-files.js";
|
|
16
17
|
/** Whether a configured string carries either special character. */
|
|
17
18
|
function isPattern(text) {
|
|
@@ -142,7 +143,7 @@ export function roots(project, held, targetRoot, options) {
|
|
|
142
143
|
for (const point of entryPoints(project, files, options.testFiles)) {
|
|
143
144
|
enter(point.file, point.rule, point.source, point.exports);
|
|
144
145
|
}
|
|
145
|
-
const exportsOf = exportTables(project, held, [
|
|
146
|
+
const { tables: exportsOf, answered } = exportTables(project, held, [
|
|
146
147
|
...entered.filter((entry) => entry.exports).map((entry) => entry.file),
|
|
147
148
|
...published,
|
|
148
149
|
]);
|
|
@@ -171,41 +172,56 @@ export function roots(project, held, targetRoot, options) {
|
|
|
171
172
|
const site = left === undefined || right === undefined ? 0 : byPosition(left, right);
|
|
172
173
|
return site || compare(a.kind, b.kind) || compare(a.source, b.source);
|
|
173
174
|
});
|
|
174
|
-
return {
|
|
175
|
+
return {
|
|
176
|
+
configFile: project.configFile,
|
|
177
|
+
liveUnderReachability: ordered,
|
|
178
|
+
unmatched,
|
|
179
|
+
unanswered: answered ? [] : held.symbols.map((symbol) => symbol.id),
|
|
180
|
+
};
|
|
175
181
|
}
|
|
176
182
|
/**
|
|
177
183
|
* Every declaration each file's module exports, by the file's name: the declaration
|
|
178
184
|
* each name of the export table is, and every link of a re-export's chain. A name
|
|
179
|
-
* whose declaration is outside the project's own files
|
|
180
|
-
* and
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
* its export table, star re-exports resolved into it. A file that is no module
|
|
184
|
-
* exports nothing.
|
|
185
|
+
* whose declaration is outside the project's own files names nothing here. One batch
|
|
186
|
+
* resolves every file to its module and one request per module reads its export
|
|
187
|
+
* table, star re-exports resolved into it. A file that is no module exports nothing.
|
|
188
|
+
* `answered` is false where the checker left a table or a link of a chain unanswered.
|
|
185
189
|
*/
|
|
186
190
|
function exportTables(project, held, wanted) {
|
|
187
191
|
const unique = [...new Map(wanted.map((file) => [file.fileName, file])).values()];
|
|
188
192
|
const tables = new Map();
|
|
193
|
+
let answered = true;
|
|
189
194
|
if (unique.length === 0) {
|
|
190
|
-
return tables;
|
|
195
|
+
return { tables, answered };
|
|
191
196
|
}
|
|
192
197
|
const modules = project.symbolsAt(unique.map((file) => project.handle(file)));
|
|
193
198
|
const chains = aliasChains(project, held);
|
|
199
|
+
const topLevel = (file) => {
|
|
200
|
+
const fileId = held.declarations.get(nodeKey(file, file));
|
|
201
|
+
return held.symbols.filter((symbol) => symbol.parent === fileId).map((symbol) => symbol.id);
|
|
202
|
+
};
|
|
194
203
|
unique.forEach((file, index) => {
|
|
195
204
|
const module = modules[index];
|
|
196
205
|
if (module === undefined) {
|
|
197
206
|
tables.set(file.fileName, []);
|
|
198
207
|
return;
|
|
199
208
|
}
|
|
209
|
+
const table = module === UNANSWERED ? UNANSWERED : project.queries.exportsOfModule(module);
|
|
210
|
+
if (table === UNANSWERED) {
|
|
211
|
+
answered = false;
|
|
212
|
+
tables.set(file.fileName, topLevel(file));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
200
215
|
const ids = new Set();
|
|
201
|
-
for (const exported of
|
|
216
|
+
for (const exported of table) {
|
|
202
217
|
for (const target of chains.chainOf(exported)) {
|
|
218
|
+
answered &&= !target.guessed;
|
|
203
219
|
ids.add(target.id);
|
|
204
220
|
}
|
|
205
221
|
}
|
|
206
222
|
tables.set(file.fileName, [...ids]);
|
|
207
223
|
});
|
|
208
|
-
return tables;
|
|
224
|
+
return { tables, answered };
|
|
209
225
|
}
|
|
210
226
|
/** The kinds of container whose export table, not its members' visibility, says what it publishes. */
|
|
211
227
|
const EXPORTING_CONTAINERS = new Set(["file", "namespace"]);
|
|
@@ -16,6 +16,7 @@ import { classMembers, memberComponent, typesAt } from "./class-members.js";
|
|
|
16
16
|
import { names, namesADeclaration, resolveEntries, spelledEntry, } from "./configured-declarations.js";
|
|
17
17
|
import { nodeKey } from "./inventory.js";
|
|
18
18
|
import { renderPosition } from "./position.js";
|
|
19
|
+
import { must } from "./query.js";
|
|
19
20
|
import { valueReach } from "./value-reach.js";
|
|
20
21
|
/** The serializer every run reads, whatever the configuration names. */
|
|
21
22
|
const JSON_STRINGIFY = { shape: "global", global: "JSON.stringify" };
|
|
@@ -185,12 +186,13 @@ function erasedParameters(project, parameters) {
|
|
|
185
186
|
if (!parameter.rest) {
|
|
186
187
|
return (type.flags & ERASED) !== 0;
|
|
187
188
|
}
|
|
188
|
-
const [element] =
|
|
189
|
+
const [element] = must(project.queries.typeArguments(type));
|
|
189
190
|
return element !== undefined && (element.flags & ERASED) !== 0;
|
|
190
191
|
});
|
|
191
192
|
const bySymbol = new Map();
|
|
192
193
|
project
|
|
193
194
|
.symbolsAt(erased.map((parameter) => project.handle(parameter.name)))
|
|
195
|
+
.map(must)
|
|
194
196
|
.forEach((symbol, index) => {
|
|
195
197
|
const parameter = erased[index];
|
|
196
198
|
if (symbol !== undefined && parameter !== undefined) {
|
|
@@ -206,11 +208,11 @@ function handedOn(project, sites, erased, invocationOf) {
|
|
|
206
208
|
.filter((value) => spellings.has(value.name.getText()))
|
|
207
209
|
.map((value) => ({ site, argument: index, value }))));
|
|
208
210
|
const plain = candidates.filter((one) => !one.value.shorthand);
|
|
209
|
-
const resolved = project.symbolsAt(plain.map((one) => project.handle(one.value.name)));
|
|
211
|
+
const resolved = project.symbolsAt(plain.map((one) => project.handle(one.value.name))).map(must);
|
|
210
212
|
const symbolOf = new Map(plain.map((one, index) => [one, resolved[index]]));
|
|
211
213
|
return candidates.flatMap((one) => {
|
|
212
214
|
const symbol = one.value.shorthand
|
|
213
|
-
? project.shorthandValueAt(project.handle(one.value.name))
|
|
215
|
+
? must(project.shorthandValueAt(project.handle(one.value.name)))
|
|
214
216
|
: symbolOf.get(one);
|
|
215
217
|
const parameter = symbol === undefined ? undefined : erased.get(symbol.id);
|
|
216
218
|
return parameter === undefined ? [] : [{ parameter, site: one.site, argument: one.argument }];
|
|
@@ -226,8 +228,8 @@ function handedOn(project, sites, erased, invocationOf) {
|
|
|
226
228
|
*/
|
|
227
229
|
function destinations(input, chains, walked) {
|
|
228
230
|
const { project, held } = input;
|
|
229
|
-
const
|
|
230
|
-
const own =
|
|
231
|
+
const queries = project.queries;
|
|
232
|
+
const own = project.ownPaths();
|
|
231
233
|
const named = resolveEntries(project, held, [JSON_STRINGIFY, ...input.ts.serializers]).filter(namesADeclaration);
|
|
232
234
|
const forms = new Map();
|
|
233
235
|
for (const site of walked.sites) {
|
|
@@ -259,7 +261,7 @@ function destinations(input, chains, walked) {
|
|
|
259
261
|
if (form !== undefined &&
|
|
260
262
|
method !== undefined &&
|
|
261
263
|
outside(method) &&
|
|
262
|
-
FUNCTION_INTERFACES.has(
|
|
264
|
+
FUNCTION_INTERFACES.has(must(project.queries.parentOf(method))?.name ?? "")) {
|
|
263
265
|
return form.invocation;
|
|
264
266
|
}
|
|
265
267
|
return { callee: call.expression, arguments: call.arguments, indirect: false };
|
|
@@ -275,14 +277,14 @@ function destinations(input, chains, walked) {
|
|
|
275
277
|
const invoked = invocation(call);
|
|
276
278
|
// Through a method of the prototype the resolved signature is the method's, so
|
|
277
279
|
// the parameters are read off the callee's own signatures, any one erasing.
|
|
278
|
-
const type = invoked.indirect ?
|
|
280
|
+
const type = invoked.indirect ? must(queries.typeAt(invoked.callee)) : undefined;
|
|
279
281
|
const signatures = invoked.indirect
|
|
280
282
|
? type === undefined
|
|
281
283
|
? []
|
|
282
|
-
:
|
|
283
|
-
: [
|
|
284
|
+
: must(queries.signaturesOf(type, SignatureKind.Call))
|
|
285
|
+
: [must(queries.resolvedSignature(call))].filter((one) => one !== undefined);
|
|
284
286
|
indexes = new Set(invoked.arguments.flatMap((_unused, index) => signatures.some((signature) => {
|
|
285
|
-
const parameter =
|
|
287
|
+
const parameter = must(queries.parameterType(signature, index));
|
|
286
288
|
return parameter !== undefined && (parameter.flags & ERASED) !== 0;
|
|
287
289
|
})
|
|
288
290
|
? [index]
|