@cplieger/deadset-ts 3.12.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/bin/node-host.js +10 -7
- package/dist/src/alias-chain.js +59 -7
- package/dist/src/analysis.js +217 -48
- package/dist/src/calls.js +23 -22
- package/dist/src/class-members.js +10 -11
- package/dist/src/components.js +4 -2
- package/dist/src/configured-declarations.js +56 -7
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/dependencies.js +34 -17
- package/dist/src/deprecation.js +35 -24
- package/dist/src/destructuring.js +37 -29
- package/dist/src/discover.js +105 -28
- package/dist/src/edges.js +4 -1
- package/dist/src/entry-points.js +7 -0
- package/dist/src/enum-group.js +46 -40
- package/dist/src/exempt.js +42 -1
- package/dist/src/findings/completion.js +37 -8
- package/dist/src/findings/emitters.js +20 -6
- package/dist/src/findings/interfaces.js +7 -1
- package/dist/src/findings/intra-function.js +91 -2
- package/dist/src/findings/self-check.js +30 -7
- package/dist/src/findings/unused-declarations.js +15 -3
- package/dist/src/findings-pass.js +18 -11
- package/dist/src/framework-lifecycle.js +8 -6
- package/dist/src/implementations.js +45 -23
- package/dist/src/interface-satisfaction.js +30 -27
- package/dist/src/intra-function-parts.js +660 -0
- package/dist/src/inventory.js +85 -4
- package/dist/src/json-chunks.js +76 -0
- package/dist/src/kinds.js +337 -35
- package/dist/src/partial.js +23 -0
- package/dist/src/query.js +207 -0
- package/dist/src/references.js +92 -20
- package/dist/src/reflective-lookup.js +46 -4
- package/dist/src/report.js +14 -3
- package/dist/src/reporters/annotations.js +9 -5
- package/dist/src/reporters/json.js +3 -2
- package/dist/src/reporters/sarif.js +22 -16
- package/dist/src/reporters/template.js +112 -12
- package/dist/src/reporters/text.js +8 -6
- package/dist/src/roots.js +27 -11
- package/dist/src/serialization-contract.js +53 -32
- package/dist/src/session.js +79 -39
- package/dist/src/signatures.js +32 -0
- package/dist/src/suppress-file.js +29 -7
- package/dist/src/suppress-inline.js +8 -1
- package/dist/src/suppress.js +27 -0
- package/dist/src/sweep.js +4 -1
- package/dist/src/value-reach.js +44 -25
- package/dist/src/verbs/analyze.js +39 -14
- package/dist/src/verbs/describe.js +12 -8
- package/dist/src/verbs/explain.js +22 -9
- package/dist/src/verbs/print-projects.js +40 -7
- package/dist/src/verbs/print-retained.js +6 -2
- package/dist/src/verbs/print-roots.js +5 -1
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +3 -3
- package/src/alias-chain.ts +75 -11
- package/src/analysis.ts +385 -104
- package/src/calls.ts +10 -9
- package/src/class-members.ts +10 -11
- package/src/components.ts +4 -2
- package/src/configured-declarations.ts +80 -8
- package/src/conformance-record.ts +2 -2
- package/src/dependencies.ts +42 -20
- package/src/deprecation.ts +53 -27
- package/src/destructuring.ts +50 -39
- package/src/discover.ts +177 -33
- package/src/edges.ts +4 -1
- package/src/entry-points.ts +7 -0
- package/src/enum-group.ts +56 -42
- package/src/exempt.ts +50 -2
- package/src/finding.ts +5 -1
- package/src/findings/completion.ts +46 -10
- package/src/findings/emitter.ts +3 -0
- package/src/findings/emitters.ts +27 -6
- package/src/findings/interfaces.ts +7 -1
- package/src/findings/intra-function.ts +106 -3
- package/src/findings/self-check.ts +48 -7
- package/src/findings/unused-declarations.ts +18 -4
- package/src/findings-pass.ts +26 -11
- package/src/framework-lifecycle.ts +10 -6
- package/src/host.ts +5 -3
- package/src/implementations.ts +59 -23
- package/src/interface-satisfaction.ts +30 -27
- package/src/intra-function-parts.ts +889 -0
- package/src/inventory.ts +97 -4
- package/src/json-chunks.ts +87 -0
- package/src/kinds.ts +355 -41
- package/src/partial.ts +40 -0
- package/src/query.ts +522 -0
- package/src/references.ts +119 -38
- package/src/reflective-lookup.ts +56 -6
- package/src/report.ts +32 -4
- package/src/reporters/annotations.ts +23 -21
- package/src/reporters/json.ts +3 -2
- package/src/reporters/reporter.ts +2 -2
- package/src/reporters/sarif.ts +27 -18
- package/src/reporters/template.ts +119 -14
- package/src/reporters/text.ts +12 -12
- package/src/roots.ts +34 -12
- package/src/serialization-contract.ts +59 -40
- package/src/session.ts +146 -58
- package/src/signatures.ts +55 -0
- package/src/suppress-file.ts +40 -8
- package/src/suppress-inline.ts +8 -1
- package/src/suppress.ts +31 -0
- package/src/sweep.ts +9 -4
- package/src/value-reach.ts +66 -28
- package/src/verbs/analyze.ts +51 -14
- package/src/verbs/describe.ts +13 -9
- package/src/verbs/explain.ts +20 -9
- package/src/verbs/print-projects.ts +53 -7
- package/src/verbs/print-retained.ts +6 -2
- package/src/verbs/print-roots.ts +5 -1
- package/src/version.ts +1 -1
- package/dist/src/reporters/rules.js +0 -178
- package/src/reporters/rules.ts +0 -195
package/src/references.ts
CHANGED
|
@@ -28,12 +28,13 @@ import {
|
|
|
28
28
|
type SourceFile,
|
|
29
29
|
} from "@typescript/native/unstable/ast";
|
|
30
30
|
import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
|
|
31
|
-
import { aliasChains } from "./alias-chain.ts";
|
|
31
|
+
import { aliasChains, type ChainTarget } from "./alias-chain.ts";
|
|
32
32
|
import { destructuring, propertyReadsOf, type PropertyRead } from "./destructuring.ts";
|
|
33
33
|
import { globExpression } from "./glob.ts";
|
|
34
|
-
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
34
|
+
import { declarationsByName, nodeKey, type Inventory } from "./inventory.ts";
|
|
35
35
|
import { relativePath } from "./paths.ts";
|
|
36
36
|
import { byPosition, renderPosition, type Position } from "./position.ts";
|
|
37
|
+
import { DEFAULT_BATCH_CAP, UNANSWERED, type Answer } from "./query.ts";
|
|
37
38
|
import type { ProjectView } from "./session.ts";
|
|
38
39
|
|
|
39
40
|
/**
|
|
@@ -63,21 +64,15 @@ import type { ProjectView } from "./session.ts";
|
|
|
63
64
|
export type Use = "read" | "write" | "evaluation" | "decorator";
|
|
64
65
|
|
|
65
66
|
/**
|
|
66
|
-
* Which
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* - `shorthand` is the per-node lookup for `{ a }`, whose name resolves to the property
|
|
73
|
-
* the literal declares rather than to the declaration that name reads.
|
|
74
|
-
* - `alias` is a step along an import or export chain, from one link to what it names.
|
|
75
|
-
* - `syntax` is the tree alone, which says what a decorator is attached to.
|
|
76
|
-
* - `destructured` is the property a pattern names, looked up on the type of the value
|
|
77
|
-
* the pattern destructures.
|
|
67
|
+
* Which path answered for one reference: `batch`, the batched lookup over a file's name
|
|
68
|
+
* nodes; `resolved-symbol`, the per-node lookup for the residue a batch left;
|
|
69
|
+
* `shorthand`, the lookup for the value `{ a }` reads; `alias`, a step along an import
|
|
70
|
+
* or export chain; `syntax`, the tree alone, which says what a decorator is attached to;
|
|
71
|
+
* `destructured`, the property a pattern names, on the destructured value's type; and
|
|
72
|
+
* `by-name`, every declaration bearing the name a site spells, where the checker failed.
|
|
78
73
|
*/
|
|
79
74
|
export type Resolution =
|
|
80
|
-
"batch" | "resolved-symbol" | "shorthand" | "alias" | "syntax" | "destructured";
|
|
75
|
+
"batch" | "resolved-symbol" | "shorthand" | "alias" | "syntax" | "destructured" | "by-name";
|
|
81
76
|
|
|
82
77
|
/** One use of one declaration by one declaration. */
|
|
83
78
|
export interface Reference {
|
|
@@ -187,13 +182,6 @@ export interface ReferenceOptions {
|
|
|
187
182
|
readonly consumer?: ConsumerSide;
|
|
188
183
|
}
|
|
189
184
|
|
|
190
|
-
/**
|
|
191
|
-
* The batch cap a run takes when the configuration names none. It bounds the size of
|
|
192
|
-
* one answer rather than the work: the median file is one batch at this cap and the
|
|
193
|
-
* largest production file a handful.
|
|
194
|
-
*/
|
|
195
|
-
export const DEFAULT_BATCH_CAP = 4096;
|
|
196
|
-
|
|
197
185
|
/**
|
|
198
186
|
* The operators whose left side one assignment stores into. A compound assignment is
|
|
199
187
|
* among them: it reads its target only to write the result back, so nothing else
|
|
@@ -564,6 +552,53 @@ export function testFileRulesOf(
|
|
|
564
552
|
.sort((a, b) => compare(a.rule, b.rule));
|
|
565
553
|
}
|
|
566
554
|
|
|
555
|
+
const importedCache = new WeakMap<
|
|
556
|
+
SourceFile,
|
|
557
|
+
ReadonlyMap<string, readonly (string | undefined)[]>
|
|
558
|
+
>();
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Each local name one file's imports bind, mapped to the names it imports: the
|
|
562
|
+
* exported name of a named import, `default` for a default import, and `undefined` for
|
|
563
|
+
* a namespace import or an import assignment, which binds a whole module.
|
|
564
|
+
*/
|
|
565
|
+
function importedNames(file: SourceFile): ReadonlyMap<string, readonly (string | undefined)[]> {
|
|
566
|
+
const known = importedCache.get(file);
|
|
567
|
+
if (known !== undefined) {
|
|
568
|
+
return known;
|
|
569
|
+
}
|
|
570
|
+
const names = new Map<string, (string | undefined)[]>();
|
|
571
|
+
const bind = (local: string, imported: string | undefined): void => {
|
|
572
|
+
names.set(local, [...(names.get(local) ?? []), imported]);
|
|
573
|
+
};
|
|
574
|
+
for (const statement of file.statements) {
|
|
575
|
+
if (isImportEqualsDeclaration(statement)) {
|
|
576
|
+
bind(statement.name.text, undefined);
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
const clause = isImportDeclaration(statement) ? statement.importClause : undefined;
|
|
580
|
+
if (clause === undefined) {
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
if (clause.name !== undefined) {
|
|
584
|
+
bind(clause.name.text, "default");
|
|
585
|
+
}
|
|
586
|
+
const bindings = clause.namedBindings;
|
|
587
|
+
if (bindings === undefined) {
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
if (bindings.kind === SyntaxKind.NamespaceImport) {
|
|
591
|
+
bind(bindings.name.text, undefined);
|
|
592
|
+
} else {
|
|
593
|
+
for (const element of bindings.elements) {
|
|
594
|
+
bind(element.name.text, element.propertyName?.text ?? element.name.text);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
importedCache.set(file, names);
|
|
599
|
+
return names;
|
|
600
|
+
}
|
|
601
|
+
|
|
567
602
|
/** Two strings ordered bytewise, which is the order every set of a run is read in. */
|
|
568
603
|
function compare(a: string, b: string): number {
|
|
569
604
|
if (a === b) {
|
|
@@ -633,17 +668,42 @@ export function references<Brand>(
|
|
|
633
668
|
* Each later link is a declaration of its own that references the next, so one link
|
|
634
669
|
* per re-export is what lets a chain be followed one declaration at a time.
|
|
635
670
|
*/
|
|
636
|
-
const carriedBy = (link: Link, symbol: TSSymbol): readonly
|
|
637
|
-
link.alias
|
|
671
|
+
const carriedBy = (link: Link, symbol: TSSymbol): readonly ChainTarget[] =>
|
|
672
|
+
link.alias
|
|
673
|
+
? chains.nextDeclared(symbol)
|
|
674
|
+
: chains.declarationsOf(symbol).map((id) => ({ id, stepped: false, guessed: false }));
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Every declaration bearing the name one node spells, or the name an import of its
|
|
678
|
+
* file binds that name to, which is what a reference the checker could not resolve is
|
|
679
|
+
* counted as: it may be a use of any of them, and a declaration it may use is kept
|
|
680
|
+
* live. A name a namespace import binds may be a use of any file.
|
|
681
|
+
*/
|
|
682
|
+
const named = (node: Node): readonly string[] => {
|
|
683
|
+
const byName = declarationsByName(held);
|
|
684
|
+
const text = node.getText();
|
|
685
|
+
const imported = importedNames(node.getSourceFile()).get(text) ?? [];
|
|
686
|
+
return [
|
|
687
|
+
...new Set([
|
|
688
|
+
...(byName.get(text) ?? []),
|
|
689
|
+
...imported.flatMap((name) =>
|
|
690
|
+
name === undefined ? [...fileIds] : (byName.get(name) ?? []),
|
|
691
|
+
),
|
|
692
|
+
]),
|
|
693
|
+
];
|
|
694
|
+
};
|
|
638
695
|
|
|
639
696
|
/** The symbol one site resolves to, asking only the accessor the site's form names. */
|
|
640
|
-
const symbolFor = (
|
|
697
|
+
const symbolFor = (
|
|
698
|
+
site: Site,
|
|
699
|
+
answered: ReadonlyMap<Node, Answer<TSSymbol>>,
|
|
700
|
+
): Answer<TSSymbol | undefined> => {
|
|
641
701
|
if (site.shorthand !== undefined) {
|
|
642
702
|
shorthandLookups += 1;
|
|
643
703
|
return project.shorthandValueAt(project.handle(site.shorthand));
|
|
644
704
|
}
|
|
645
705
|
const batched = answered.get(site.node);
|
|
646
|
-
if (batched
|
|
706
|
+
if (batched === UNANSWERED || typeof batched === "object") {
|
|
647
707
|
return batched;
|
|
648
708
|
}
|
|
649
709
|
residueFallbacks += 1;
|
|
@@ -688,11 +748,14 @@ export function references<Brand>(
|
|
|
688
748
|
...evaluations.map((evaluation) => evaluation.node),
|
|
689
749
|
]),
|
|
690
750
|
];
|
|
691
|
-
const answered = new Map<Node, TSSymbol
|
|
751
|
+
const answered = new Map<Node, Answer<TSSymbol>>();
|
|
692
752
|
batched += batching.length;
|
|
693
753
|
for (let from = 0; from < batching.length; from += cap) {
|
|
694
754
|
const run = batching.slice(from, from + cap);
|
|
695
|
-
const answers = project.symbolsAt(
|
|
755
|
+
const answers = project.symbolsAt(
|
|
756
|
+
run.map((node) => project.handle(node)),
|
|
757
|
+
cap,
|
|
758
|
+
);
|
|
696
759
|
fileBatches += 1;
|
|
697
760
|
run.forEach((node, index) => {
|
|
698
761
|
const symbol = answers[index];
|
|
@@ -708,13 +771,17 @@ export function references<Brand>(
|
|
|
708
771
|
continue;
|
|
709
772
|
}
|
|
710
773
|
const position = renderPosition(file, root, link.at.getStart());
|
|
711
|
-
|
|
774
|
+
const carried =
|
|
775
|
+
symbol === UNANSWERED
|
|
776
|
+
? named(link.at).map((id) => ({ id, stepped: false, guessed: true }))
|
|
777
|
+
: carriedBy(link, symbol);
|
|
778
|
+
for (const target of carried) {
|
|
712
779
|
found.push({
|
|
713
780
|
from: link.from,
|
|
714
|
-
to: id,
|
|
781
|
+
to: target.id,
|
|
715
782
|
position,
|
|
716
783
|
use: "read",
|
|
717
|
-
resolution: link.alias ? "alias" : "batch",
|
|
784
|
+
resolution: target.guessed ? "by-name" : link.alias ? "alias" : "batch",
|
|
718
785
|
test,
|
|
719
786
|
});
|
|
720
787
|
}
|
|
@@ -726,14 +793,16 @@ export function references<Brand>(
|
|
|
726
793
|
continue;
|
|
727
794
|
}
|
|
728
795
|
const position = renderPosition(file, root, evaluation.node.getStart());
|
|
729
|
-
|
|
796
|
+
// A module the checker could not resolve may be any file of the target.
|
|
797
|
+
const evaluated = module === UNANSWERED ? [...fileIds] : chains.declarationsOf(module);
|
|
798
|
+
for (const id of evaluated) {
|
|
730
799
|
if (fileIds.has(id)) {
|
|
731
800
|
found.push({
|
|
732
801
|
from: evaluation.from,
|
|
733
802
|
to: id,
|
|
734
803
|
position,
|
|
735
804
|
use: "evaluation",
|
|
736
|
-
resolution: "batch",
|
|
805
|
+
resolution: module === UNANSWERED ? "by-name" : "batch",
|
|
737
806
|
test,
|
|
738
807
|
});
|
|
739
808
|
}
|
|
@@ -748,13 +817,21 @@ export function references<Brand>(
|
|
|
748
817
|
return;
|
|
749
818
|
}
|
|
750
819
|
const position = renderPosition(file, root, read.key.getStart());
|
|
751
|
-
|
|
820
|
+
const targets =
|
|
821
|
+
symbol === UNANSWERED
|
|
822
|
+
? (declarationsByName(held).get(read.name) ?? []).map((id) => ({
|
|
823
|
+
id,
|
|
824
|
+
stepped: false,
|
|
825
|
+
guessed: true,
|
|
826
|
+
}))
|
|
827
|
+
: chains.chainOf(symbol);
|
|
828
|
+
for (const target of targets) {
|
|
752
829
|
found.push({
|
|
753
830
|
from: read.from,
|
|
754
831
|
to: target.id,
|
|
755
832
|
position,
|
|
756
833
|
use: "read",
|
|
757
|
-
resolution: "destructured",
|
|
834
|
+
resolution: target.guessed ? "by-name" : "destructured",
|
|
758
835
|
test,
|
|
759
836
|
});
|
|
760
837
|
}
|
|
@@ -764,7 +841,7 @@ export function references<Brand>(
|
|
|
764
841
|
const direct: Resolution =
|
|
765
842
|
site.shorthand !== undefined
|
|
766
843
|
? "shorthand"
|
|
767
|
-
: answered.
|
|
844
|
+
: typeof answered.get(site.node) === "object"
|
|
768
845
|
? "batch"
|
|
769
846
|
: "resolved-symbol";
|
|
770
847
|
const symbol = symbolFor(site, answered);
|
|
@@ -774,13 +851,17 @@ export function references<Brand>(
|
|
|
774
851
|
const position = renderPosition(file, root, site.node.getStart());
|
|
775
852
|
// A re-export something imports is used and so is the declaration behind it, so
|
|
776
853
|
// the use names every link of the chain its name stands at the head of.
|
|
777
|
-
|
|
854
|
+
const targets =
|
|
855
|
+
symbol === UNANSWERED
|
|
856
|
+
? named(site.node).map((id) => ({ id, stepped: false, guessed: true }))
|
|
857
|
+
: chains.chainOf(symbol);
|
|
858
|
+
for (const target of targets) {
|
|
778
859
|
found.push({
|
|
779
860
|
from: site.from,
|
|
780
861
|
to: target.id,
|
|
781
862
|
position,
|
|
782
863
|
use: site.use,
|
|
783
|
-
resolution: target.stepped ? "alias" : direct,
|
|
864
|
+
resolution: target.guessed ? "by-name" : target.stepped ? "alias" : direct,
|
|
784
865
|
test,
|
|
785
866
|
});
|
|
786
867
|
}
|
package/src/reflective-lookup.ts
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* string reaches every member of that name on any type, static members included,
|
|
9
9
|
* and every record names the string's own position. A key that is not literal text
|
|
10
10
|
* holds nothing back: a computed name is the case no exact rule serves. `Reflect` is
|
|
11
|
-
* the global one only: a local declaration of that name is a different object.
|
|
11
|
+
* the global one only: a local declaration of that name is a different object. An
|
|
12
|
+
* element access through a receiver whose type declares a string index signature, and is
|
|
13
|
+
* not `any`, reads that signature and holds nothing back.
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
16
|
import {
|
|
@@ -21,9 +23,12 @@ import {
|
|
|
21
23
|
type Node,
|
|
22
24
|
type SourceFile,
|
|
23
25
|
} from "@typescript/native/unstable/ast";
|
|
26
|
+
import { TypeFlags, type Type } from "@typescript/native/unstable/sync";
|
|
24
27
|
import type { DetectorInput, Evidence } from "./exempt.ts";
|
|
25
28
|
import { memberNames } from "./member-names.ts";
|
|
26
29
|
import { renderPosition } from "./position.ts";
|
|
30
|
+
import { isAnswered, UNANSWERED } from "./query.ts";
|
|
31
|
+
import type { ProjectView } from "./session.ts";
|
|
27
32
|
|
|
28
33
|
/** The `Reflect` methods whose second argument is a property key. */
|
|
29
34
|
const REFLECT_KEYED: ReadonlySet<string> = new Set(["get", "set", "has"]);
|
|
@@ -35,6 +40,8 @@ interface Lookup {
|
|
|
35
40
|
readonly callee: string;
|
|
36
41
|
/** The `Reflect` the call names, where the lookup is a `Reflect` call. */
|
|
37
42
|
readonly reflect: Node | undefined;
|
|
43
|
+
/** The expression an element access reads from, where the lookup is one. */
|
|
44
|
+
readonly receiver: Node | undefined;
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
/** The text one key denotes, where it is literal text. */
|
|
@@ -57,6 +64,7 @@ function lookupsOf(file: SourceFile): readonly Lookup[] {
|
|
|
57
64
|
name,
|
|
58
65
|
callee: `[${JSON.stringify(name)}]`,
|
|
59
66
|
reflect: undefined,
|
|
67
|
+
receiver: node.expression,
|
|
60
68
|
});
|
|
61
69
|
}
|
|
62
70
|
} else if (isCallExpression(node) && isPropertyAccessExpression(node.expression)) {
|
|
@@ -72,7 +80,13 @@ function lookupsOf(file: SourceFile): readonly Lookup[] {
|
|
|
72
80
|
key !== undefined &&
|
|
73
81
|
name !== undefined
|
|
74
82
|
) {
|
|
75
|
-
found.push({
|
|
83
|
+
found.push({
|
|
84
|
+
key,
|
|
85
|
+
name,
|
|
86
|
+
callee: `Reflect.${method.text}`,
|
|
87
|
+
reflect: target,
|
|
88
|
+
receiver: undefined,
|
|
89
|
+
});
|
|
76
90
|
}
|
|
77
91
|
}
|
|
78
92
|
node.forEachChild(visit);
|
|
@@ -81,6 +95,19 @@ function lookupsOf(file: SourceFile): readonly Lookup[] {
|
|
|
81
95
|
return found;
|
|
82
96
|
}
|
|
83
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Whether a receiver's type is not `any` and declares a string index signature. A type
|
|
100
|
+
* whose index signatures the checker did not answer for is read as declaring none, so
|
|
101
|
+
* the lookup retains what it names.
|
|
102
|
+
*/
|
|
103
|
+
function readsAMapping<Brand>(project: ProjectView<Brand>, type: Type): boolean {
|
|
104
|
+
if ((type.flags & TypeFlags.Any) !== 0) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
const infos = project.queries.indexInfosOf(type);
|
|
108
|
+
return isAnswered(infos) && infos.some((info) => (info.keyType.flags & TypeFlags.String) !== 0);
|
|
109
|
+
}
|
|
110
|
+
|
|
84
111
|
/**
|
|
85
112
|
* The reflective-lookup detector: every member whose name a literal key of a lookup
|
|
86
113
|
* spells, recorded at the key.
|
|
@@ -95,7 +122,7 @@ export function reflectiveLookup<Brand>(input: DetectorInput<Brand>): readonly E
|
|
|
95
122
|
|
|
96
123
|
// Whether each written `Reflect` is the global one, asked in one batch: a
|
|
97
124
|
// declaration of that name in one of the project's own files shadows it.
|
|
98
|
-
const ownFiles =
|
|
125
|
+
const ownFiles = project.ownPaths();
|
|
99
126
|
const reflects = lookups.flatMap((lookup) =>
|
|
100
127
|
lookup.reflect === undefined ? [] : [lookup.reflect],
|
|
101
128
|
);
|
|
@@ -103,21 +130,44 @@ export function reflectiveLookup<Brand>(input: DetectorInput<Brand>): readonly E
|
|
|
103
130
|
if (reflects.length > 0) {
|
|
104
131
|
project.symbolsAt(reflects.map((node) => project.handle(node))).forEach((symbol, index) => {
|
|
105
132
|
const node = reflects[index];
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
133
|
+
// A name the checker did not answer for is read as the global, which retains.
|
|
134
|
+
const declaredHere =
|
|
135
|
+
symbol === UNANSWERED
|
|
136
|
+
? false
|
|
137
|
+
: symbol?.declarations.some((declaration) => ownFiles.has(declaration.path));
|
|
109
138
|
if (node !== undefined && declaredHere === false) {
|
|
110
139
|
global.add(node);
|
|
111
140
|
}
|
|
112
141
|
});
|
|
113
142
|
}
|
|
114
143
|
|
|
144
|
+
// An element access through a receiver that maps every string key to one value type
|
|
145
|
+
// reads that mapping rather than a member, asked in one batch.
|
|
146
|
+
const receivers = lookups.flatMap((lookup) =>
|
|
147
|
+
lookup.receiver === undefined ? [] : [lookup.receiver],
|
|
148
|
+
);
|
|
149
|
+
const mapped = new Set<Node>();
|
|
150
|
+
project.queries.typesAt(receivers).forEach((type, index) => {
|
|
151
|
+
const receiver = receivers[index];
|
|
152
|
+
if (
|
|
153
|
+
receiver !== undefined &&
|
|
154
|
+
type !== undefined &&
|
|
155
|
+
isAnswered(type) &&
|
|
156
|
+
readsAMapping(project, type)
|
|
157
|
+
) {
|
|
158
|
+
mapped.add(receiver);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
115
162
|
const members = memberNames(files, held);
|
|
116
163
|
const found: Evidence[] = [];
|
|
117
164
|
for (const lookup of lookups) {
|
|
118
165
|
if (lookup.reflect !== undefined && !global.has(lookup.reflect)) {
|
|
119
166
|
continue;
|
|
120
167
|
}
|
|
168
|
+
if (lookup.receiver !== undefined && mapped.has(lookup.receiver)) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
121
171
|
const site = renderPosition(lookup.key.getSourceFile(), targetRoot, lookup.key.getStart());
|
|
122
172
|
for (const member of members.get(lookup.name) ?? []) {
|
|
123
173
|
found.push({ id: member.id, detail: `looked up by ${lookup.callee}`, site });
|
package/src/report.ts
CHANGED
|
@@ -18,7 +18,13 @@ import type { StaleSuppression } from "./findings/self-check.ts";
|
|
|
18
18
|
import type { TestFileRule } from "./references.ts";
|
|
19
19
|
|
|
20
20
|
/** The version of the report schema this analyzer writes a report to. */
|
|
21
|
-
|
|
21
|
+
const SCHEMA_VERSION = "6.1.0";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Every report schema version this analyzer reads: the schema versions the Contract it
|
|
25
|
+
* implements admits, each an instance of the one it writes.
|
|
26
|
+
*/
|
|
27
|
+
export const SCHEMA_VERSIONS_ACCEPTED: readonly string[] = ["6.0.0", SCHEMA_VERSION];
|
|
22
28
|
|
|
23
29
|
/** The name this analyzer writes every document under and mints every identifier with. */
|
|
24
30
|
export const ANALYZER_NAME = "deadset-ts";
|
|
@@ -49,6 +55,7 @@ export interface WireDetails {
|
|
|
49
55
|
readonly path?: string;
|
|
50
56
|
readonly reason?: string;
|
|
51
57
|
};
|
|
58
|
+
readonly overlap?: readonly string[];
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
/** One finding as the finding schema spells it. */
|
|
@@ -73,6 +80,11 @@ export interface WireFinding {
|
|
|
73
80
|
readonly root: boolean;
|
|
74
81
|
readonly symbol_count: number;
|
|
75
82
|
readonly deletable_lines: number;
|
|
83
|
+
readonly members?: readonly {
|
|
84
|
+
readonly ref: string;
|
|
85
|
+
readonly name: string;
|
|
86
|
+
readonly position: WirePosition;
|
|
87
|
+
}[];
|
|
76
88
|
};
|
|
77
89
|
readonly retained_by: readonly string[];
|
|
78
90
|
readonly configurations: readonly string[];
|
|
@@ -130,6 +142,15 @@ export interface ReportConfiguration {
|
|
|
130
142
|
readonly project: string;
|
|
131
143
|
}
|
|
132
144
|
|
|
145
|
+
/** One project configuration the run derived and could not build, with why. */
|
|
146
|
+
export interface ReportNotBuilt {
|
|
147
|
+
readonly id: string;
|
|
148
|
+
/** The compiler configuration file below the target root. */
|
|
149
|
+
readonly project: string;
|
|
150
|
+
/** The first line of the error that dropped it, naming no path outside the target. */
|
|
151
|
+
readonly error: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
133
154
|
/** One consumer the run loaded, at the directory a report names it by. */
|
|
134
155
|
interface ReportConsumer {
|
|
135
156
|
readonly id: string;
|
|
@@ -160,7 +181,7 @@ export interface Report {
|
|
|
160
181
|
};
|
|
161
182
|
readonly target: { readonly kind: TargetKind; readonly root: string; readonly identity: string };
|
|
162
183
|
readonly configurations: readonly ReportConfiguration[];
|
|
163
|
-
readonly configurations_not_built: readonly
|
|
184
|
+
readonly configurations_not_built: readonly ReportNotBuilt[];
|
|
164
185
|
readonly consumers: {
|
|
165
186
|
readonly declared: number;
|
|
166
187
|
readonly loaded: readonly {
|
|
@@ -191,6 +212,7 @@ export interface ReportInput {
|
|
|
191
212
|
readonly declaredGaps: readonly DeclaredGap[];
|
|
192
213
|
readonly target: { readonly kind: TargetKind; readonly root: string; readonly identity: string };
|
|
193
214
|
readonly configurations: readonly ReportConfiguration[];
|
|
215
|
+
readonly notBuilt: readonly ReportNotBuilt[];
|
|
194
216
|
readonly loaded: readonly ReportConsumer[];
|
|
195
217
|
readonly unavailable: readonly UnavailableConsumer[];
|
|
196
218
|
readonly result: PassResult;
|
|
@@ -247,6 +269,7 @@ function wireDetails(details: FindingDetails): WireDetails {
|
|
|
247
269
|
...(entry.reason === undefined ? {} : { reason: entry.reason }),
|
|
248
270
|
},
|
|
249
271
|
}),
|
|
272
|
+
...(details.overlap === undefined ? {} : { overlap: details.overlap }),
|
|
250
273
|
};
|
|
251
274
|
}
|
|
252
275
|
|
|
@@ -275,6 +298,9 @@ export function wireFinding(finding: CompletedFinding): WireFinding {
|
|
|
275
298
|
root: finding.component.root,
|
|
276
299
|
symbol_count: finding.component.symbolCount,
|
|
277
300
|
deletable_lines: finding.component.deletableLines,
|
|
301
|
+
...(finding.component.members === undefined
|
|
302
|
+
? {}
|
|
303
|
+
: { members: finding.component.members.map(wireSymbol) }),
|
|
278
304
|
},
|
|
279
305
|
retained_by: finding.retainedBy,
|
|
280
306
|
configurations: finding.configurations,
|
|
@@ -409,7 +435,7 @@ export function buildReport(input: ReportInput): Report {
|
|
|
409
435
|
name: ANALYZER_NAME,
|
|
410
436
|
version: input.version,
|
|
411
437
|
languages: [LANGUAGE],
|
|
412
|
-
schema_versions_accepted:
|
|
438
|
+
schema_versions_accepted: SCHEMA_VERSIONS_ACCEPTED,
|
|
413
439
|
conformance: {
|
|
414
440
|
corpus_version: input.conformance.corpusVersion,
|
|
415
441
|
result: input.conformance.result,
|
|
@@ -420,7 +446,9 @@ export function buildReport(input: ReportInput): Report {
|
|
|
420
446
|
configurations: [...input.configurations]
|
|
421
447
|
.sort((a, b) => compare(a.id, b.id))
|
|
422
448
|
.map((one) => ({ id: one.id, project: one.project })),
|
|
423
|
-
configurations_not_built: []
|
|
449
|
+
configurations_not_built: [...input.notBuilt]
|
|
450
|
+
.sort((a, b) => compare(a.id, b.id))
|
|
451
|
+
.map((one) => ({ id: one.id, project: one.project, error: one.error })),
|
|
424
452
|
consumers: { declared: loaded.length + unavailable.length, loaded, unavailable },
|
|
425
453
|
findings,
|
|
426
454
|
edge_evaluations: input.result.edgeEvaluations.map(wireEvaluation).sort(byEdge),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fails } from "../config.ts";
|
|
2
|
+
import { coalesced } from "../json-chunks.ts";
|
|
2
3
|
import { KINDS } from "../kinds.ts";
|
|
3
4
|
import type { Report } from "../report.ts";
|
|
4
5
|
import type { RenderOptions } from "./reporter.ts";
|
|
@@ -42,26 +43,27 @@ function command(
|
|
|
42
43
|
* failing severity is an error and one below it a warning; a stale suppression is an error,
|
|
43
44
|
* because its kind is fixed on at the failing severity.
|
|
44
45
|
*/
|
|
45
|
-
export function annotations(report: Report, options: RenderOptions): string {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
export function annotations(report: Report, options: RenderOptions): Iterable<string> {
|
|
47
|
+
return coalesced(
|
|
48
|
+
(function* lines(): Generator<string, void, undefined> {
|
|
49
|
+
for (const found of report.findings) {
|
|
50
|
+
yield `${command(
|
|
51
|
+
fails(found.severity, options.failOn) ? "error" : "warning",
|
|
52
|
+
found.position,
|
|
53
|
+
found.position.end_line,
|
|
54
|
+
`${found.code} ${found.kind}`,
|
|
55
|
+
found.message,
|
|
56
|
+
)}\n`;
|
|
57
|
+
}
|
|
58
|
+
for (const stale of report.stale_suppressions) {
|
|
59
|
+
yield `${command(
|
|
60
|
+
"error",
|
|
61
|
+
stale.position,
|
|
62
|
+
stale.position.line,
|
|
63
|
+
`${stale.code} ${kindName(stale.code)}`,
|
|
64
|
+
stale.message,
|
|
65
|
+
)}\n`;
|
|
66
|
+
}
|
|
67
|
+
})(),
|
|
54
68
|
);
|
|
55
|
-
for (const stale of report.stale_suppressions) {
|
|
56
|
-
lines.push(
|
|
57
|
-
command(
|
|
58
|
-
"error",
|
|
59
|
-
stale.position,
|
|
60
|
-
stale.position.line,
|
|
61
|
-
`${stale.code} ${kindName(stale.code)}`,
|
|
62
|
-
stale.message,
|
|
63
|
-
),
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
return lines.map((line) => `${line}\n`).join("");
|
|
67
69
|
}
|
package/src/reporters/json.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { jsonChunks } from "../json-chunks.ts";
|
|
1
2
|
import type { Report } from "../report.ts";
|
|
2
3
|
|
|
3
4
|
/** The indentation of the report document, which with the schema's member order fixes its bytes. */
|
|
4
5
|
const INDENT = 2;
|
|
5
6
|
|
|
6
|
-
/** The report document itself: the report, indented, with a closing newline. */
|
|
7
|
-
export const json = (report: Report): string =>
|
|
7
|
+
/** The report document itself, in pieces: the report, indented, with a closing newline. */
|
|
8
|
+
export const json = (report: Report): Iterable<string> => jsonChunks(report, INDENT);
|
|
@@ -12,8 +12,8 @@ export interface RenderOptions {
|
|
|
12
12
|
readonly template: Template | undefined;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
/** One rendering of a report, as the
|
|
16
|
-
type Reporter = (report: Report, options: RenderOptions) => string
|
|
15
|
+
/** One rendering of a report, as the pieces of the file it is written to, in order. */
|
|
16
|
+
type Reporter = (report: Report, options: RenderOptions) => Iterable<string>;
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* One format this analyzer renders: its reporter, and the suffix appended to the report's
|
package/src/reporters/sarif.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Confidence, Severity } from "../config.ts";
|
|
2
|
-
import {
|
|
2
|
+
import { jsonChunks } from "../json-chunks.ts";
|
|
3
|
+
import { KINDS, type KindRow } from "../kinds.ts";
|
|
3
4
|
import type { Report, WireFinding, WireStaleSuppression } from "../report.ts";
|
|
4
5
|
import { lineHashes, symbolFingerprint } from "./fingerprint.ts";
|
|
5
6
|
import { RenderError, type RenderOptions } from "./reporter.ts";
|
|
6
|
-
import { RULE_TEXTS } from "./rules.ts";
|
|
7
7
|
import { utf8Bytes } from "./sha256.ts";
|
|
8
8
|
|
|
9
9
|
const SCHEMA =
|
|
@@ -12,7 +12,7 @@ const VERSION = "2.1.0";
|
|
|
12
12
|
/** The unit a finding's column counts. */
|
|
13
13
|
const COLUMN_KIND = "utf16CodeUnits";
|
|
14
14
|
const URI_BASE_ID = "%SRCROOT%";
|
|
15
|
-
const ROOT_DESCRIPTION = "The target root
|
|
15
|
+
const ROOT_DESCRIPTION = "The target root.";
|
|
16
16
|
|
|
17
17
|
/** The most related locations one result carries; the report stays complete beyond them. */
|
|
18
18
|
const MAX_RELATED = 100;
|
|
@@ -41,23 +41,26 @@ function firstSentence(text: string): string {
|
|
|
41
41
|
return text;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/** Every live kind whose languages include one of the run's, in code order. */
|
|
45
|
+
function kindsOf(languages: readonly string[]): readonly KindRow[] {
|
|
46
|
+
return [...KINDS.values()].filter((kind) =>
|
|
47
|
+
kind.languages.some((language) => languages.includes(language)),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
44
51
|
/**
|
|
45
|
-
* One rule per live kind of
|
|
52
|
+
* One rule per live kind of the run's languages, in code order, whether or not a result
|
|
46
53
|
* names it, so a rule's index is the same in every run and under every configuration.
|
|
47
54
|
*/
|
|
48
|
-
function rules(): readonly Record<string, unknown>[] {
|
|
49
|
-
return
|
|
50
|
-
const kind = KINDS.get(code);
|
|
51
|
-
if (kind === undefined) {
|
|
52
|
-
throw new RenderError(`the rule ${code} names no live kind`);
|
|
53
|
-
}
|
|
55
|
+
function rules(kinds: readonly KindRow[]): readonly Record<string, unknown>[] {
|
|
56
|
+
return kinds.map((kind) => {
|
|
54
57
|
return {
|
|
55
|
-
id: code,
|
|
58
|
+
id: kind.code,
|
|
56
59
|
name: kind.name,
|
|
57
|
-
shortDescription: { text: firstSentence(
|
|
58
|
-
fullDescription: { text:
|
|
60
|
+
shortDescription: { text: firstSentence(kind.rule) },
|
|
61
|
+
fullDescription: { text: kind.rule },
|
|
59
62
|
help: {
|
|
60
|
-
text:
|
|
63
|
+
text: kind.precondition === undefined ? kind.rule : `${kind.rule}\n\n${kind.precondition}`,
|
|
61
64
|
},
|
|
62
65
|
defaultConfiguration: { level: LEVEL[kind.defaultSeverity] },
|
|
63
66
|
properties: {
|
|
@@ -155,6 +158,11 @@ function relatedOf(found: WireFinding): readonly Related[] {
|
|
|
155
158
|
for (const at of found.details.write_positions ?? []) {
|
|
156
159
|
related.push({ label: "write", at, endLine: at.end_line });
|
|
157
160
|
}
|
|
161
|
+
for (const one of found.component.members ?? []) {
|
|
162
|
+
if (one.ref !== found.symbol.ref) {
|
|
163
|
+
related.push({ label: "member", at: one.position, endLine: one.position.end_line });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
158
166
|
return related.slice(0, MAX_RELATED);
|
|
159
167
|
}
|
|
160
168
|
|
|
@@ -253,9 +261,10 @@ function staleResult(
|
|
|
253
261
|
* count it. A file the rendering cannot read, or a line it does not hold, fails the
|
|
254
262
|
* rendering, because a line fingerprint computed from other bytes opens a second alert.
|
|
255
263
|
*/
|
|
256
|
-
export function sarif(report: Report, options: RenderOptions): string {
|
|
257
|
-
const
|
|
258
|
-
const
|
|
264
|
+
export function sarif(report: Report, options: RenderOptions): Iterable<string> {
|
|
265
|
+
const kinds = kindsOf(report.analyzer.languages);
|
|
266
|
+
const ruleList = rules(kinds);
|
|
267
|
+
const index = new Map(kinds.map((kind, at) => [kind.code, at]));
|
|
259
268
|
const hashOf = lineHashCache(options.readSource);
|
|
260
269
|
const results = [
|
|
261
270
|
...report.findings.map((found) => findingResult(found, index, hashOf)),
|
|
@@ -283,5 +292,5 @@ export function sarif(report: Report, options: RenderOptions): string {
|
|
|
283
292
|
},
|
|
284
293
|
],
|
|
285
294
|
};
|
|
286
|
-
return
|
|
295
|
+
return jsonChunks(log, INDENT);
|
|
287
296
|
}
|