@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/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
|
@@ -24,10 +24,10 @@ import {
|
|
|
24
24
|
type SourceFile,
|
|
25
25
|
} from "@typescript/native/unstable/ast";
|
|
26
26
|
import { TypeFlags, type Type } from "@typescript/native/unstable/sync";
|
|
27
|
-
import { typesAt } from "./class-members.ts";
|
|
28
27
|
import type { DetectorInput, Evidence } from "./exempt.ts";
|
|
29
28
|
import { memberNames } from "./member-names.ts";
|
|
30
29
|
import { renderPosition } from "./position.ts";
|
|
30
|
+
import { isAnswered, UNANSWERED } from "./query.ts";
|
|
31
31
|
import type { ProjectView } from "./session.ts";
|
|
32
32
|
|
|
33
33
|
/** The `Reflect` methods whose second argument is a property key. */
|
|
@@ -95,14 +95,17 @@ function lookupsOf(file: SourceFile): readonly Lookup[] {
|
|
|
95
95
|
return found;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
/**
|
|
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
|
+
*/
|
|
99
103
|
function readsAMapping<Brand>(project: ProjectView<Brand>, type: Type): boolean {
|
|
100
104
|
if ((type.flags & TypeFlags.Any) !== 0) {
|
|
101
105
|
return false;
|
|
102
106
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
.some((info) => (info.keyType.flags & TypeFlags.String) !== 0);
|
|
107
|
+
const infos = project.queries.indexInfosOf(type);
|
|
108
|
+
return isAnswered(infos) && infos.some((info) => (info.keyType.flags & TypeFlags.String) !== 0);
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
/**
|
|
@@ -119,7 +122,7 @@ export function reflectiveLookup<Brand>(input: DetectorInput<Brand>): readonly E
|
|
|
119
122
|
|
|
120
123
|
// Whether each written `Reflect` is the global one, asked in one batch: a
|
|
121
124
|
// declaration of that name in one of the project's own files shadows it.
|
|
122
|
-
const ownFiles =
|
|
125
|
+
const ownFiles = project.ownPaths();
|
|
123
126
|
const reflects = lookups.flatMap((lookup) =>
|
|
124
127
|
lookup.reflect === undefined ? [] : [lookup.reflect],
|
|
125
128
|
);
|
|
@@ -127,9 +130,11 @@ export function reflectiveLookup<Brand>(input: DetectorInput<Brand>): readonly E
|
|
|
127
130
|
if (reflects.length > 0) {
|
|
128
131
|
project.symbolsAt(reflects.map((node) => project.handle(node))).forEach((symbol, index) => {
|
|
129
132
|
const node = reflects[index];
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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));
|
|
133
138
|
if (node !== undefined && declaredHere === false) {
|
|
134
139
|
global.add(node);
|
|
135
140
|
}
|
|
@@ -142,9 +147,14 @@ export function reflectiveLookup<Brand>(input: DetectorInput<Brand>): readonly E
|
|
|
142
147
|
lookup.receiver === undefined ? [] : [lookup.receiver],
|
|
143
148
|
);
|
|
144
149
|
const mapped = new Set<Node>();
|
|
145
|
-
typesAt(
|
|
150
|
+
project.queries.typesAt(receivers).forEach((type, index) => {
|
|
146
151
|
const receiver = receivers[index];
|
|
147
|
-
if (
|
|
152
|
+
if (
|
|
153
|
+
receiver !== undefined &&
|
|
154
|
+
type !== undefined &&
|
|
155
|
+
isAnswered(type) &&
|
|
156
|
+
readsAMapping(project, type)
|
|
157
|
+
) {
|
|
148
158
|
mapped.add(receiver);
|
|
149
159
|
}
|
|
150
160
|
});
|
package/src/report.ts
CHANGED
|
@@ -142,6 +142,15 @@ export interface ReportConfiguration {
|
|
|
142
142
|
readonly project: string;
|
|
143
143
|
}
|
|
144
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
|
+
|
|
145
154
|
/** One consumer the run loaded, at the directory a report names it by. */
|
|
146
155
|
interface ReportConsumer {
|
|
147
156
|
readonly id: string;
|
|
@@ -172,7 +181,7 @@ export interface Report {
|
|
|
172
181
|
};
|
|
173
182
|
readonly target: { readonly kind: TargetKind; readonly root: string; readonly identity: string };
|
|
174
183
|
readonly configurations: readonly ReportConfiguration[];
|
|
175
|
-
readonly configurations_not_built: readonly
|
|
184
|
+
readonly configurations_not_built: readonly ReportNotBuilt[];
|
|
176
185
|
readonly consumers: {
|
|
177
186
|
readonly declared: number;
|
|
178
187
|
readonly loaded: readonly {
|
|
@@ -203,6 +212,7 @@ export interface ReportInput {
|
|
|
203
212
|
readonly declaredGaps: readonly DeclaredGap[];
|
|
204
213
|
readonly target: { readonly kind: TargetKind; readonly root: string; readonly identity: string };
|
|
205
214
|
readonly configurations: readonly ReportConfiguration[];
|
|
215
|
+
readonly notBuilt: readonly ReportNotBuilt[];
|
|
206
216
|
readonly loaded: readonly ReportConsumer[];
|
|
207
217
|
readonly unavailable: readonly UnavailableConsumer[];
|
|
208
218
|
readonly result: PassResult;
|
|
@@ -436,7 +446,9 @@ export function buildReport(input: ReportInput): Report {
|
|
|
436
446
|
configurations: [...input.configurations]
|
|
437
447
|
.sort((a, b) => compare(a.id, b.id))
|
|
438
448
|
.map((one) => ({ id: one.id, project: one.project })),
|
|
439
|
-
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 })),
|
|
440
452
|
consumers: { declared: loaded.length + unavailable.length, loaded, unavailable },
|
|
441
453
|
findings,
|
|
442
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,4 +1,5 @@
|
|
|
1
1
|
import type { Confidence, Severity } from "../config.ts";
|
|
2
|
+
import { jsonChunks } from "../json-chunks.ts";
|
|
2
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";
|
|
@@ -260,7 +261,7 @@ function staleResult(
|
|
|
260
261
|
* count it. A file the rendering cannot read, or a line it does not hold, fails the
|
|
261
262
|
* rendering, because a line fingerprint computed from other bytes opens a second alert.
|
|
262
263
|
*/
|
|
263
|
-
export function sarif(report: Report, options: RenderOptions): string {
|
|
264
|
+
export function sarif(report: Report, options: RenderOptions): Iterable<string> {
|
|
264
265
|
const kinds = kindsOf(report.analyzer.languages);
|
|
265
266
|
const ruleList = rules(kinds);
|
|
266
267
|
const index = new Map(kinds.map((kind, at) => [kind.code, at]));
|
|
@@ -291,5 +292,5 @@ export function sarif(report: Report, options: RenderOptions): string {
|
|
|
291
292
|
},
|
|
292
293
|
],
|
|
293
294
|
};
|
|
294
|
-
return
|
|
295
|
+
return jsonChunks(log, INDENT);
|
|
295
296
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* and a member the document does not carry fails the rendering.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { CHUNK_LENGTH } from "../json-chunks.ts";
|
|
10
11
|
import { RenderError } from "./reporter.ts";
|
|
11
12
|
import { utf8Bytes } from "./sha256.ts";
|
|
12
13
|
|
|
@@ -849,12 +850,24 @@ function call(name: string, args: readonly Value[]): Value {
|
|
|
849
850
|
/** One execution of a template over a document. */
|
|
850
851
|
class Execution {
|
|
851
852
|
private readonly scopes: Map<string, Value>[];
|
|
852
|
-
|
|
853
|
+
/** The rendering so far, in pieces of about {@link CHUNK_LENGTH}, the last still growing. */
|
|
854
|
+
readonly pieces: string[] = [""];
|
|
853
855
|
|
|
854
856
|
constructor(root: Value) {
|
|
855
857
|
this.scopes = [new Map([["$", root]])];
|
|
856
858
|
}
|
|
857
859
|
|
|
860
|
+
private write(text: string): void {
|
|
861
|
+
const last = this.pieces.length - 1;
|
|
862
|
+
const held = (this.pieces[last] ?? "") + text;
|
|
863
|
+
if (held.length >= CHUNK_LENGTH) {
|
|
864
|
+
this.pieces[last] = held;
|
|
865
|
+
this.pieces.push("");
|
|
866
|
+
} else {
|
|
867
|
+
this.pieces[last] = held;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
858
871
|
private lookup(name: string): Value {
|
|
859
872
|
for (let i = this.scopes.length - 1; i >= 0; i -= 1) {
|
|
860
873
|
const scope = this.scopes[i];
|
|
@@ -963,12 +976,12 @@ class Execution {
|
|
|
963
976
|
for (const node of nodes) {
|
|
964
977
|
switch (node.k) {
|
|
965
978
|
case "text":
|
|
966
|
-
this.
|
|
979
|
+
this.write(node.text);
|
|
967
980
|
break;
|
|
968
981
|
case "action": {
|
|
969
982
|
const value = this.pipeline(node.pipe, dot, true);
|
|
970
983
|
if (node.pipe.vars.length === 0) {
|
|
971
|
-
this.
|
|
984
|
+
this.write(formatted(value, false));
|
|
972
985
|
}
|
|
973
986
|
break;
|
|
974
987
|
}
|
|
@@ -1041,12 +1054,41 @@ class Execution {
|
|
|
1041
1054
|
}
|
|
1042
1055
|
|
|
1043
1056
|
/**
|
|
1044
|
-
*
|
|
1045
|
-
*
|
|
1057
|
+
* One document as the JSON value it encodes: what `JSON.stringify` would write for it,
|
|
1058
|
+
* read back, without writing it. A member holding nothing is left out, and an array
|
|
1059
|
+
* element holding nothing, like a number that is not finite, is null.
|
|
1060
|
+
*/
|
|
1061
|
+
function plainOf(value: unknown): Value | undefined {
|
|
1062
|
+
if (Array.isArray(value)) {
|
|
1063
|
+
return value.map((element: unknown) => plainOf(element) ?? null);
|
|
1064
|
+
}
|
|
1065
|
+
if (typeof value === "object" && value !== null) {
|
|
1066
|
+
const held: Record<string, Value> = {};
|
|
1067
|
+
for (const [key, member] of Object.entries(value)) {
|
|
1068
|
+
const plain = plainOf(member);
|
|
1069
|
+
if (plain !== undefined) {
|
|
1070
|
+
held[key] = plain;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
return held;
|
|
1074
|
+
}
|
|
1075
|
+
if (typeof value === "number") {
|
|
1076
|
+
return Number.isFinite(value) ? value : null;
|
|
1077
|
+
}
|
|
1078
|
+
if (typeof value === "string" || typeof value === "boolean" || value === null) {
|
|
1079
|
+
return value;
|
|
1080
|
+
}
|
|
1081
|
+
return undefined;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* The template's rendering of one document, in pieces of about {@link CHUNK_LENGTH}.
|
|
1086
|
+
* The rendering is complete before it is answered, so a template that fails partway
|
|
1087
|
+
* renders nothing, and the failure names what it could not evaluate.
|
|
1046
1088
|
*/
|
|
1047
|
-
export function renderTemplate(template: Template, document: unknown): string {
|
|
1048
|
-
const root =
|
|
1089
|
+
export function renderTemplate(template: Template, document: unknown): readonly string[] {
|
|
1090
|
+
const root = plainOf(document) ?? null;
|
|
1049
1091
|
const execution = new Execution(root);
|
|
1050
1092
|
execution.run(template.nodes, root);
|
|
1051
|
-
return execution.
|
|
1093
|
+
return execution.pieces.filter((piece) => piece !== "");
|
|
1052
1094
|
}
|
package/src/reporters/text.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { coalesced } from "../json-chunks.ts";
|
|
1
2
|
import type { Report, WireTotals } from "../report.ts";
|
|
2
3
|
|
|
3
4
|
/** The kind token and the confidence a stale suppression is rendered with, both fixed. */
|
|
@@ -29,16 +30,15 @@ function summary(totals: WireTotals): string {
|
|
|
29
30
|
* summary is deliberately not in that shape, so a filter for finding lines yields exactly
|
|
30
31
|
* them. Nothing else is written: no timestamp, duration or host detail.
|
|
31
32
|
*/
|
|
32
|
-
export const text = (report: Report): string =>
|
|
33
|
-
|
|
34
|
-
(
|
|
35
|
-
|
|
33
|
+
export const text = (report: Report): Iterable<string> =>
|
|
34
|
+
coalesced(
|
|
35
|
+
(function* lines(): Generator<string, void, undefined> {
|
|
36
|
+
for (const found of report.findings) {
|
|
37
|
+
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`;
|
|
38
|
+
}
|
|
39
|
+
for (const stale of report.stale_suppressions) {
|
|
40
|
+
yield `${stale.position.path}:${String(stale.position.line)}:${String(stale.position.column)}: ${STALE_KIND} ${stale.symbol}: ${stale.message} [${STALE_CONFIDENCE}] (${stale.code})\n`;
|
|
41
|
+
}
|
|
42
|
+
yield `${summary(report.totals)}\n`;
|
|
43
|
+
})(),
|
|
36
44
|
);
|
|
37
|
-
for (const stale of report.stale_suppressions) {
|
|
38
|
-
lines.push(
|
|
39
|
-
`${stale.position.path}:${String(stale.position.line)}:${String(stale.position.column)}: ${STALE_KIND} ${stale.symbol}: ${stale.message} [${STALE_CONFIDENCE}] (${stale.code})`,
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
lines.push(summary(report.totals));
|
|
43
|
-
return `${lines.join("\n")}\n`;
|
|
44
|
-
};
|