@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
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* call sites that use or discard it, each statement control flow cannot reach, each
|
|
5
5
|
* write to a local that no read reaches, and each case an earlier case covers.
|
|
6
6
|
*/
|
|
7
|
-
import { isArrowFunction, isAwaitExpression, isBigIntLiteral, isBinaryExpression, isBlock, isBreakStatement, isCallExpression, isCaseClause, isClassDeclaration, isClassExpression, isContinueStatement, isDecorator, isDefaultClause, isEmptyStatement, isExportAssignment, isExportSpecifier, isExpressionStatement, isFunctionDeclaration, isHeritageClause, isIdentifier, isIfStatement, isImportClause, isImportSpecifier, isInterfaceDeclaration, isMethodDeclaration, isModuleDeclaration, isNamespaceImport, isNoSubstitutionTemplateLiteral, isNumericLiteral, isParenthesizedExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isReturnStatement, isShorthandPropertyAssignment, isStringLiteral, isSwitchStatement, isThrowStatement, isTypeAliasDeclaration, isVariableStatement, isVoidExpression, NodeFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
7
|
+
import { isArrowFunction, isAwaitExpression, isBigIntLiteral, isBinaryExpression, isBlock, isBreakStatement, isCallExpression, isCaseClause, isClassDeclaration, isClassExpression, isContinueStatement, isDecorator, isDefaultClause, isEmptyStatement, isExportAssignment, isExportSpecifier, isExpressionStatement, isFunctionDeclaration, isHeritageClause, isIdentifier, isIfStatement, isImportClause, isImportSpecifier, isInterfaceDeclaration, isMethodDeclaration, isModuleDeclaration, isNamespaceImport, isNoSubstitutionTemplateLiteral, isNumericLiteral, isParenthesizedExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isQualifiedName, isReturnStatement, isShorthandPropertyAssignment, isStringLiteral, isSwitchStatement, isThrowStatement, isTypeAliasDeclaration, isTypeReferenceNode, isVariableStatement, isVoidExpression, NodeFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
8
8
|
import { nodeKey } from "./inventory.js";
|
|
9
9
|
import { positionKey, renderPosition } from "./position.js";
|
|
10
|
-
import {
|
|
10
|
+
import { isAnswered, UNANSWERED } from "./query.js";
|
|
11
11
|
import { signatureOf } from "./signatures.js";
|
|
12
12
|
/** The codes of the parts this module reads. */
|
|
13
13
|
export const UNUSED_PARAMETER = "DS1801";
|
|
@@ -195,8 +195,15 @@ function extendedNames(files) {
|
|
|
195
195
|
const visit = (node) => {
|
|
196
196
|
if (isHeritageClause(node) && node.token === SyntaxKind.ExtendsKeyword) {
|
|
197
197
|
for (const type of node.types) {
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
// A class's clause holds expressions; an interface's holds type references.
|
|
199
|
+
let named;
|
|
200
|
+
if (isTypeReferenceNode(type)) {
|
|
201
|
+
named = isQualifiedName(type.typeName) ? type.typeName.right : type.typeName;
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
const expression = unwrapped(type.expression);
|
|
205
|
+
named = isPropertyAccessExpression(expression) ? expression.name : expression;
|
|
206
|
+
}
|
|
200
207
|
if (isIdentifier(named)) {
|
|
201
208
|
names.add(named.text);
|
|
202
209
|
}
|
|
@@ -230,31 +237,33 @@ function hierarchyFixes(declared, extended) {
|
|
|
230
237
|
}
|
|
231
238
|
return owner.name !== undefined && extended.has(owner.name.text);
|
|
232
239
|
}
|
|
233
|
-
/**
|
|
240
|
+
/**
|
|
241
|
+
* The symbol identifier each of a batch of identifiers resolves to, where the batch
|
|
242
|
+
* resolves it, and {@link UNANSWERED} where the checker did not answer.
|
|
243
|
+
*/
|
|
234
244
|
function resolvedIds(project, nodes) {
|
|
235
245
|
const ids = new Map();
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
}
|
|
246
|
+
project.symbolsAt(nodes.map((node) => project.handle(node))).forEach((symbol, index) => {
|
|
247
|
+
const node = nodes[index];
|
|
248
|
+
if (node !== undefined && symbol !== undefined) {
|
|
249
|
+
ids.set(node, symbol === UNANSWERED ? UNANSWERED : symbol.id);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
245
252
|
return ids;
|
|
246
253
|
}
|
|
247
254
|
/**
|
|
248
255
|
* Whether one identifier reads the binding a name node declares: it resolves to the
|
|
249
|
-
* same symbol, or the checker resolves it to nothing
|
|
250
|
-
* risk a finding; a shorthand property reads
|
|
256
|
+
* same symbol, or the checker resolves it to nothing or does not answer for either
|
|
257
|
+
* name, which is read as a use rather than risk a finding; a shorthand property reads
|
|
258
|
+
* the binding of its own name.
|
|
251
259
|
*/
|
|
252
260
|
function readsBinding(use, declared, ids) {
|
|
253
261
|
if (isShorthandPropertyAssignment(use.parent)) {
|
|
254
262
|
return true;
|
|
255
263
|
}
|
|
256
264
|
const at = ids.get(use);
|
|
257
|
-
|
|
265
|
+
const own = ids.get(declared);
|
|
266
|
+
return at === undefined || at === UNANSWERED || own === UNANSWERED || at === own;
|
|
258
267
|
}
|
|
259
268
|
/**
|
|
260
269
|
* The parts of one project's own declarations. `references` are the project's own
|
|
@@ -575,9 +584,17 @@ function resultParts(project, root, sites, references, calls) {
|
|
|
575
584
|
if (!discardedAt.has(id) || generator || !returnsAValue(signature)) {
|
|
576
585
|
continue;
|
|
577
586
|
}
|
|
578
|
-
|
|
579
|
-
const
|
|
580
|
-
if (
|
|
587
|
+
// A question the checker leaves unanswered withholds the finding.
|
|
588
|
+
const typed = project.queries.signatureOf(signature);
|
|
589
|
+
if (typed === undefined || !isAnswered(typed)) {
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
const result = project.queries.returnTypeOf(typed);
|
|
593
|
+
if (result === undefined || !isAnswered(result)) {
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
const spelled = project.queries.typeToString(result);
|
|
597
|
+
if (!isAnswered(spelled) || NO_VALUE.has(spelled)) {
|
|
581
598
|
continue;
|
|
582
599
|
}
|
|
583
600
|
const at = signature.type ?? node.name ?? node;
|
package/dist/src/inventory.js
CHANGED
|
@@ -2,6 +2,7 @@ import { isClassDeclaration, isComputedPropertyName, isEnumDeclaration, isEnumMe
|
|
|
2
2
|
import { dirnamePath, joinPath, relativePath } from "./paths.js";
|
|
3
3
|
import { byPosition, positionKey, renderPosition } from "./position.js";
|
|
4
4
|
import { computedComponent, nameComponent, renderRef } from "./ref.js";
|
|
5
|
+
import { isAnswered, UNANSWERED } from "./query.js";
|
|
5
6
|
const MANIFEST = "package.json";
|
|
6
7
|
/**
|
|
7
8
|
* Resolves each file's package scope: the name of the nearest manifest at or above
|
|
@@ -493,12 +494,28 @@ export function inventory(project, host, targetRoot) {
|
|
|
493
494
|
const handles = batched.map((node) => project.handle(node));
|
|
494
495
|
const resolved = project.symbolsAt(handles);
|
|
495
496
|
const symbolOf = new Map();
|
|
497
|
+
// A scope or a container whose symbol or table went unanswered keeps what its syntax
|
|
498
|
+
// declares, and everything under it is recorded unanswered.
|
|
499
|
+
const unansweredOwners = new Set();
|
|
496
500
|
batched.forEach((node, index) => {
|
|
497
501
|
const symbol = resolved[index];
|
|
498
|
-
if (symbol
|
|
502
|
+
if (typeof symbol === "object") {
|
|
499
503
|
symbolOf.set(node, symbol);
|
|
500
504
|
}
|
|
501
505
|
});
|
|
506
|
+
resolved.slice(0, exportScopes.length).forEach((symbol, index) => {
|
|
507
|
+
const scope = exportScopes[index];
|
|
508
|
+
if (symbol === UNANSWERED && scope !== undefined) {
|
|
509
|
+
unansweredOwners.add(scope.owner);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
containers
|
|
513
|
+
.filter((container) => container.nameNode !== undefined)
|
|
514
|
+
.forEach((container, index) => {
|
|
515
|
+
if (resolved[exportScopes.length + index] === UNANSWERED) {
|
|
516
|
+
unansweredOwners.add(container.owner);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
502
519
|
let exportTables = 0;
|
|
503
520
|
for (const scope of exportScopes) {
|
|
504
521
|
const symbol = symbolOf.get(scope.nameNode);
|
|
@@ -506,7 +523,11 @@ export function inventory(project, host, targetRoot) {
|
|
|
506
523
|
continue;
|
|
507
524
|
}
|
|
508
525
|
exportTables += 1;
|
|
509
|
-
|
|
526
|
+
const table = project.queries.exportsOf(symbol);
|
|
527
|
+
if (!isAnswered(table)) {
|
|
528
|
+
unansweredOwners.add(scope.owner);
|
|
529
|
+
}
|
|
530
|
+
for (const [, exported] of isAnswered(table) ? table : []) {
|
|
510
531
|
for (const declaration of exported.declarations) {
|
|
511
532
|
const held = project.declarationAt(declaration);
|
|
512
533
|
if (held === undefined) {
|
|
@@ -527,11 +548,23 @@ export function inventory(project, host, targetRoot) {
|
|
|
527
548
|
if (symbol !== undefined) {
|
|
528
549
|
if (!container.membersAreExports) {
|
|
529
550
|
memberTables += 1;
|
|
530
|
-
|
|
551
|
+
const table = project.queries.membersOf(symbol);
|
|
552
|
+
if (isAnswered(table)) {
|
|
553
|
+
outsideOwnFiles += readTable(container, table, seen, false);
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
unansweredOwners.add(container.owner);
|
|
557
|
+
}
|
|
531
558
|
}
|
|
532
559
|
if (container.membersAreExports || container.isClass) {
|
|
533
560
|
memberTables += 1;
|
|
534
|
-
|
|
561
|
+
const table = project.queries.exportsOf(symbol);
|
|
562
|
+
if (isAnswered(table)) {
|
|
563
|
+
outsideOwnFiles += readTable(container, table, seen, container.isClass);
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
unansweredOwners.add(container.owner);
|
|
567
|
+
}
|
|
535
568
|
}
|
|
536
569
|
}
|
|
537
570
|
completeFromTree(container, seen);
|
|
@@ -635,6 +668,7 @@ export function inventory(project, host, targetRoot) {
|
|
|
635
668
|
}
|
|
636
669
|
return undefined;
|
|
637
670
|
}
|
|
671
|
+
const byId = new Map(building.map((record) => [positionKey(record.position), record]));
|
|
638
672
|
const symbols = building
|
|
639
673
|
.map(render)
|
|
640
674
|
.sort((a, b) => byPosition(a.position, b.position) || (a.ref < b.ref ? -1 : 1));
|
|
@@ -645,8 +679,47 @@ export function inventory(project, host, targetRoot) {
|
|
|
645
679
|
cost: { batches: 1, exportTables, memberTables },
|
|
646
680
|
starReExports,
|
|
647
681
|
outsideOwnFiles,
|
|
682
|
+
unanswered: symbols
|
|
683
|
+
.filter((symbol) => underUnanswered(byId.get(symbol.id), unansweredOwners))
|
|
684
|
+
.map((symbol) => symbol.id),
|
|
685
|
+
byName: namesOf(building),
|
|
648
686
|
};
|
|
649
687
|
}
|
|
688
|
+
/** Whether one declaration is, or is declared under, a scope or container left unanswered. */
|
|
689
|
+
function underUnanswered(record, owners) {
|
|
690
|
+
for (let at = record; at !== undefined; at = at.parent) {
|
|
691
|
+
if (owners.has(at)) {
|
|
692
|
+
return true;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
697
|
+
/** Every declaration but a file, by each name a use may spell it with. */
|
|
698
|
+
function namesOf(building) {
|
|
699
|
+
const byName = new Map();
|
|
700
|
+
for (const record of building) {
|
|
701
|
+
if (record.kind === "file") {
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
const id = positionKey(record.position);
|
|
705
|
+
const names = record.typeParameter === undefined
|
|
706
|
+
? [record.component.text, record.localName]
|
|
707
|
+
: [record.typeParameter];
|
|
708
|
+
for (const name of new Set(names)) {
|
|
709
|
+
if (name === undefined) {
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
const ids = byName.get(name);
|
|
713
|
+
if (ids === undefined) {
|
|
714
|
+
byName.set(name, [id]);
|
|
715
|
+
}
|
|
716
|
+
else if (!ids.includes(id)) {
|
|
717
|
+
ids.push(id);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return byName;
|
|
722
|
+
}
|
|
650
723
|
/** The chain of components that names one declaration, read from the module inward. */
|
|
651
724
|
function chainOf(record) {
|
|
652
725
|
return [...record.parentChain, record.component];
|
|
@@ -691,3 +764,11 @@ function refOf(record, chain) {
|
|
|
691
764
|
...(record.typeParameter === undefined ? {} : { typeParameter: record.typeParameter }),
|
|
692
765
|
});
|
|
693
766
|
}
|
|
767
|
+
/**
|
|
768
|
+
* Every declaration of an inventory other than a file, by each name a use may spell it
|
|
769
|
+
* with. It is how a reference the checker could not resolve is still counted, as a
|
|
770
|
+
* reference to every declaration that bears the name it spells.
|
|
771
|
+
*/
|
|
772
|
+
export function declarationsByName(held) {
|
|
773
|
+
return held.byName;
|
|
774
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A document written in pieces, so no stage of writing it holds the whole document as
|
|
3
|
+
* one string: a string has a length limit far below what a large report reaches.
|
|
4
|
+
*/
|
|
5
|
+
/** The length a piece grows to before it is handed on. */
|
|
6
|
+
export const CHUNK_LENGTH = 1 << 16;
|
|
7
|
+
/**
|
|
8
|
+
* The pieces of `pieces` joined into runs of about {@link CHUNK_LENGTH}, the last one
|
|
9
|
+
* shorter. A piece longer than that is handed on whole.
|
|
10
|
+
*/
|
|
11
|
+
export function* coalesced(pieces) {
|
|
12
|
+
let held = [];
|
|
13
|
+
let length = 0;
|
|
14
|
+
for (const piece of pieces) {
|
|
15
|
+
held.push(piece);
|
|
16
|
+
length += piece.length;
|
|
17
|
+
if (length >= CHUNK_LENGTH) {
|
|
18
|
+
yield held.join("");
|
|
19
|
+
held = [];
|
|
20
|
+
length = 0;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (length > 0) {
|
|
24
|
+
yield held.join("");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Whether `JSON.stringify` writes nothing for a value where an object member holds it. */
|
|
28
|
+
function omitted(value) {
|
|
29
|
+
return value === undefined || typeof value === "function" || typeof value === "symbol";
|
|
30
|
+
}
|
|
31
|
+
function* valueOf(value, prefix, step) {
|
|
32
|
+
// With no indentation the document is compact: no line breaks, and no space after a key.
|
|
33
|
+
const nl = step === "" ? "" : "\n";
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
if (value.length === 0) {
|
|
36
|
+
yield "[]";
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const inner = prefix + step;
|
|
40
|
+
yield "[";
|
|
41
|
+
for (let at = 0; at < value.length; at += 1) {
|
|
42
|
+
yield at === 0 ? `${nl}${inner}` : `,${nl}${inner}`;
|
|
43
|
+
const element = value[at];
|
|
44
|
+
yield* omitted(element) ? ["null"] : valueOf(element, inner, step);
|
|
45
|
+
}
|
|
46
|
+
yield `${nl}${prefix}]`;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (typeof value === "object" && value !== null) {
|
|
50
|
+
const inner = prefix + step;
|
|
51
|
+
let first = true;
|
|
52
|
+
for (const [key, member] of Object.entries(value)) {
|
|
53
|
+
if (omitted(member)) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
yield `${first ? "{" : ","}${nl}${inner}${JSON.stringify(key)}:${step === "" ? "" : " "}`;
|
|
57
|
+
first = false;
|
|
58
|
+
yield* valueOf(member, inner, step);
|
|
59
|
+
}
|
|
60
|
+
yield first ? "{}" : `${nl}${prefix}}`;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
yield JSON.stringify(value);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* One JSON document in pieces whose concatenation is the value as `JSON.stringify`
|
|
67
|
+
* writes it at `indent` spaces, followed by a newline. The value is plain data: objects,
|
|
68
|
+
* arrays, strings, finite numbers, booleans and null, an object member holding nothing
|
|
69
|
+
* being left out as `JSON.stringify` leaves it out.
|
|
70
|
+
*/
|
|
71
|
+
export function jsonChunks(value, indent) {
|
|
72
|
+
return coalesced((function* document() {
|
|
73
|
+
yield* valueOf(value, "", " ".repeat(indent));
|
|
74
|
+
yield "\n";
|
|
75
|
+
})());
|
|
76
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** A count with its noun, so a line reads for one as well as for several. */
|
|
2
|
+
function counted(n, one, many) {
|
|
3
|
+
return `${String(n)} ${n === 1 ? one : many}`;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* One line per configuration the run dropped, and one counting the questions the checker
|
|
7
|
+
* left unanswered by project, so a reader of the error stream sees where the analysis was
|
|
8
|
+
* partial. The lines carry no prefix; the caller writes them as its own diagnostics.
|
|
9
|
+
*/
|
|
10
|
+
export function partialNotes(partial) {
|
|
11
|
+
const notes = [...partial.notBuilt]
|
|
12
|
+
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
|
13
|
+
.map((one) => `the derived configuration ${one.id} was not built and is not analyzed: ${one.error}`);
|
|
14
|
+
if (partial.unanswered.length > 0) {
|
|
15
|
+
const byProject = new Map();
|
|
16
|
+
for (const question of partial.unanswered) {
|
|
17
|
+
byProject.set(question.configFile, (byProject.get(question.configFile) ?? 0) + 1);
|
|
18
|
+
}
|
|
19
|
+
const where = [...byProject].map(([configFile, n]) => `${configFile}: ${String(n)}`).join(", ");
|
|
20
|
+
notes.push(`the checker answered ${counted(partial.unanswered.length, "question", "questions")} with a failure (${where}); every declaration an answer could have kept live is kept live and reported by nothing`);
|
|
21
|
+
}
|
|
22
|
+
return notes;
|
|
23
|
+
}
|
|
@@ -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
|
+
}
|