@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
|
@@ -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
|
|
|
@@ -115,7 +116,11 @@ const ESCAPES: Readonly<Record<string, string>> = {
|
|
|
115
116
|
const ESCAPE_WIDTHS: Readonly<Record<string, number>> = { x: 2, u: 4, U: 8 };
|
|
116
117
|
|
|
117
118
|
const IDENTIFIER = /[A-Za-z_][A-Za-z0-9_]*/uy;
|
|
118
|
-
|
|
119
|
+
/** A number as the lexer reads one, before the subset decides whether it is a decimal integer. */
|
|
120
|
+
const NUMBER = /[+-]?[0-9][0-9A-Za-z_.]*/uy;
|
|
121
|
+
|
|
122
|
+
/** The one number form the subset admits: a decimal integer with no leading zero. */
|
|
123
|
+
const DECIMAL_INTEGER = /^[+-]?(?:0|[1-9][0-9]*)$/u;
|
|
119
124
|
|
|
120
125
|
function lineOf(source: string, at: number): number {
|
|
121
126
|
let line = 1;
|
|
@@ -224,6 +229,11 @@ function lexAction(
|
|
|
224
229
|
NUMBER.lastIndex = i;
|
|
225
230
|
const number = NUMBER.exec(source)?.[0];
|
|
226
231
|
if (number !== undefined) {
|
|
232
|
+
if (!DECIMAL_INTEGER.test(number)) {
|
|
233
|
+
throw new TemplateError(
|
|
234
|
+
`line ${String(line)}: ${JSON.stringify(number)} is not a decimal integer`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
227
237
|
tokens.push({ t: "lit", value: Number(number) });
|
|
228
238
|
i += number.length;
|
|
229
239
|
continue;
|
|
@@ -636,7 +646,61 @@ function compareBasic(name: string, a: Value, b: Value): number {
|
|
|
636
646
|
if (kind === "boolean") {
|
|
637
647
|
return Number.NaN;
|
|
638
648
|
}
|
|
639
|
-
|
|
649
|
+
if (kind === "string") {
|
|
650
|
+
return byCodePoints(a as string, b as string);
|
|
651
|
+
}
|
|
652
|
+
return (a as number) < (b as number) ? -1 : 1;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Two strings ordered by the bytes of their UTF-8 encodings, which is the order of their
|
|
657
|
+
* code points: the order of their UTF-16 code units differs above U+FFFF.
|
|
658
|
+
*/
|
|
659
|
+
function byCodePoints(a: string, b: string): number {
|
|
660
|
+
let at = 0;
|
|
661
|
+
while (at < a.length && at < b.length) {
|
|
662
|
+
const x = a.codePointAt(at) ?? 0;
|
|
663
|
+
const y = b.codePointAt(at) ?? 0;
|
|
664
|
+
if (x !== y) {
|
|
665
|
+
return x < y ? -1 : 1;
|
|
666
|
+
}
|
|
667
|
+
at += x > 0xffff ? 2 : 1;
|
|
668
|
+
}
|
|
669
|
+
return a.length - b.length;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/** The escapes `%q` writes for the control characters that have one of their own. */
|
|
673
|
+
const QUOTED_ESCAPES: Readonly<Record<string, string>> = {
|
|
674
|
+
'"': '\\"',
|
|
675
|
+
"\\": "\\\\",
|
|
676
|
+
"\u0007": "\\a",
|
|
677
|
+
"\b": "\\b",
|
|
678
|
+
"\f": "\\f",
|
|
679
|
+
"\n": "\\n",
|
|
680
|
+
"\r": "\\r",
|
|
681
|
+
"\t": "\\t",
|
|
682
|
+
"\v": "\\v",
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* The text `%q` writes: double quotes around the text, a quote and a backslash escaped,
|
|
687
|
+
* each control character with an escape of its own written as it, every other character
|
|
688
|
+
* below U+0020 and U+007F written as `\x` and two lowercase hexadecimal digits, and every
|
|
689
|
+
* other character as itself.
|
|
690
|
+
*/
|
|
691
|
+
function goQuoted(text: string): string {
|
|
692
|
+
let out = '"';
|
|
693
|
+
for (const char of text) {
|
|
694
|
+
const code = char.codePointAt(0) ?? 0;
|
|
695
|
+
if (Object.hasOwn(QUOTED_ESCAPES, char)) {
|
|
696
|
+
out += QUOTED_ESCAPES[char] ?? "";
|
|
697
|
+
} else if (code < 0x20 || code === 0x7f) {
|
|
698
|
+
out += `\\x${code.toString(16).padStart(2, "0")}`;
|
|
699
|
+
} else {
|
|
700
|
+
out += char;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return `${out}"`;
|
|
640
704
|
}
|
|
641
705
|
|
|
642
706
|
/** `printf`'s verbs, each over one operand. */
|
|
@@ -663,10 +727,10 @@ function printf(format: string, args: readonly Value[]): string {
|
|
|
663
727
|
switch (verb) {
|
|
664
728
|
case "v":
|
|
665
729
|
case "s":
|
|
666
|
-
out += formatted(arg,
|
|
730
|
+
out += formatted(arg, true);
|
|
667
731
|
break;
|
|
668
732
|
case "q":
|
|
669
|
-
out +=
|
|
733
|
+
out += goQuoted(formatted(arg, true));
|
|
670
734
|
break;
|
|
671
735
|
case "d":
|
|
672
736
|
if (typeof arg !== "number" || !Number.isInteger(arg)) {
|
|
@@ -768,11 +832,11 @@ function call(name: string, args: readonly Value[]): Value {
|
|
|
768
832
|
return args
|
|
769
833
|
.map((arg, i) => {
|
|
770
834
|
const spaced = i > 0 && typeof arg !== "string" && typeof args[i - 1] !== "string";
|
|
771
|
-
return `${spaced ? " " : ""}${formatted(arg,
|
|
835
|
+
return `${spaced ? " " : ""}${formatted(arg, true)}`;
|
|
772
836
|
})
|
|
773
837
|
.join("");
|
|
774
838
|
case "println":
|
|
775
|
-
return `${args.map((arg) => formatted(arg,
|
|
839
|
+
return `${args.map((arg) => formatted(arg, true)).join(" ")}\n`;
|
|
776
840
|
case "printf":
|
|
777
841
|
if (typeof first !== "string") {
|
|
778
842
|
throw new RenderError("printf takes a format string first");
|
|
@@ -786,12 +850,24 @@ function call(name: string, args: readonly Value[]): Value {
|
|
|
786
850
|
/** One execution of a template over a document. */
|
|
787
851
|
class Execution {
|
|
788
852
|
private readonly scopes: Map<string, Value>[];
|
|
789
|
-
|
|
853
|
+
/** The rendering so far, in pieces of about {@link CHUNK_LENGTH}, the last still growing. */
|
|
854
|
+
readonly pieces: string[] = [""];
|
|
790
855
|
|
|
791
856
|
constructor(root: Value) {
|
|
792
857
|
this.scopes = [new Map([["$", root]])];
|
|
793
858
|
}
|
|
794
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
|
+
|
|
795
871
|
private lookup(name: string): Value {
|
|
796
872
|
for (let i = this.scopes.length - 1; i >= 0; i -= 1) {
|
|
797
873
|
const scope = this.scopes[i];
|
|
@@ -900,12 +976,12 @@ class Execution {
|
|
|
900
976
|
for (const node of nodes) {
|
|
901
977
|
switch (node.k) {
|
|
902
978
|
case "text":
|
|
903
|
-
this.
|
|
979
|
+
this.write(node.text);
|
|
904
980
|
break;
|
|
905
981
|
case "action": {
|
|
906
982
|
const value = this.pipeline(node.pipe, dot, true);
|
|
907
983
|
if (node.pipe.vars.length === 0) {
|
|
908
|
-
this.
|
|
984
|
+
this.write(formatted(value, false));
|
|
909
985
|
}
|
|
910
986
|
break;
|
|
911
987
|
}
|
|
@@ -978,12 +1054,41 @@ class Execution {
|
|
|
978
1054
|
}
|
|
979
1055
|
|
|
980
1056
|
/**
|
|
981
|
-
*
|
|
982
|
-
*
|
|
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.
|
|
983
1088
|
*/
|
|
984
|
-
export function renderTemplate(template: Template, document: unknown): string {
|
|
985
|
-
const root =
|
|
1089
|
+
export function renderTemplate(template: Template, document: unknown): readonly string[] {
|
|
1090
|
+
const root = plainOf(document) ?? null;
|
|
986
1091
|
const execution = new Execution(root);
|
|
987
1092
|
execution.run(template.nodes, root);
|
|
988
|
-
return execution.
|
|
1093
|
+
return execution.pieces.filter((piece) => piece !== "");
|
|
989
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
|
-
};
|
package/src/roots.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { globExpression } from "./glob.ts";
|
|
|
15
15
|
import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
|
|
16
16
|
import type { Manifest, ManifestEntry } from "./manifest.ts";
|
|
17
17
|
import { byPosition } from "./position.ts";
|
|
18
|
+
import { UNANSWERED } from "./query.ts";
|
|
18
19
|
import type { ProjectView } from "./session.ts";
|
|
19
20
|
import { sourceFilesOf } from "./source-files.ts";
|
|
20
21
|
|
|
@@ -70,6 +71,12 @@ export interface Roots {
|
|
|
70
71
|
* {@link unmatchedEverywhere} is what turns one into the other.
|
|
71
72
|
*/
|
|
72
73
|
readonly unmatched: readonly string[];
|
|
74
|
+
/**
|
|
75
|
+
* Every declaration of the project, where the checker left the module or export table
|
|
76
|
+
* of an entry or a published file unanswered, and none otherwise: which of them that
|
|
77
|
+
* file exports is unknown.
|
|
78
|
+
*/
|
|
79
|
+
readonly unanswered: readonly string[];
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
/** What decides the roots beside the declarations themselves. */
|
|
@@ -225,7 +232,7 @@ export function roots<Brand>(
|
|
|
225
232
|
enter(point.file, point.rule, point.source, point.exports);
|
|
226
233
|
}
|
|
227
234
|
|
|
228
|
-
const exportsOf = exportTables(project, held, [
|
|
235
|
+
const { tables: exportsOf, answered } = exportTables(project, held, [
|
|
229
236
|
...entered.filter((entry) => entry.exports).map((entry) => entry.file),
|
|
230
237
|
...published,
|
|
231
238
|
]);
|
|
@@ -258,47 +265,62 @@ export function roots<Brand>(
|
|
|
258
265
|
return site || compare(a.kind, b.kind) || compare(a.source, b.source);
|
|
259
266
|
});
|
|
260
267
|
|
|
261
|
-
return {
|
|
268
|
+
return {
|
|
269
|
+
configFile: project.configFile,
|
|
270
|
+
liveUnderReachability: ordered,
|
|
271
|
+
unmatched,
|
|
272
|
+
unanswered: answered ? [] : held.symbols.map((symbol) => symbol.id),
|
|
273
|
+
};
|
|
262
274
|
}
|
|
263
275
|
|
|
264
276
|
/**
|
|
265
277
|
* Every declaration each file's module exports, by the file's name: the declaration
|
|
266
278
|
* each name of the export table is, and every link of a re-export's chain. A name
|
|
267
|
-
* whose declaration is outside the project's own files
|
|
268
|
-
* and
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
* its export table, star re-exports resolved into it. A file that is no module
|
|
272
|
-
* exports nothing.
|
|
279
|
+
* whose declaration is outside the project's own files names nothing here. One batch
|
|
280
|
+
* resolves every file to its module and one request per module reads its export
|
|
281
|
+
* table, star re-exports resolved into it. A file that is no module exports nothing.
|
|
282
|
+
* `answered` is false where the checker left a table or a link of a chain unanswered.
|
|
273
283
|
*/
|
|
274
284
|
function exportTables<Brand>(
|
|
275
285
|
project: ProjectView<Brand>,
|
|
276
286
|
held: Inventory,
|
|
277
287
|
wanted: readonly SourceFile[],
|
|
278
|
-
): ReadonlyMap<string, readonly string[]
|
|
288
|
+
): { readonly tables: ReadonlyMap<string, readonly string[]>; readonly answered: boolean } {
|
|
279
289
|
const unique = [...new Map(wanted.map((file) => [file.fileName, file])).values()];
|
|
280
290
|
const tables = new Map<string, readonly string[]>();
|
|
291
|
+
let answered = true;
|
|
281
292
|
if (unique.length === 0) {
|
|
282
|
-
return tables;
|
|
293
|
+
return { tables, answered };
|
|
283
294
|
}
|
|
284
295
|
const modules = project.symbolsAt(unique.map((file) => project.handle(file)));
|
|
285
296
|
const chains = aliasChains(project, held);
|
|
286
297
|
|
|
298
|
+
const topLevel = (file: SourceFile): readonly string[] => {
|
|
299
|
+
const fileId = held.declarations.get(nodeKey(file, file));
|
|
300
|
+
return held.symbols.filter((symbol) => symbol.parent === fileId).map((symbol) => symbol.id);
|
|
301
|
+
};
|
|
287
302
|
unique.forEach((file, index) => {
|
|
288
303
|
const module = modules[index];
|
|
289
304
|
if (module === undefined) {
|
|
290
305
|
tables.set(file.fileName, []);
|
|
291
306
|
return;
|
|
292
307
|
}
|
|
308
|
+
const table = module === UNANSWERED ? UNANSWERED : project.queries.exportsOfModule(module);
|
|
309
|
+
if (table === UNANSWERED) {
|
|
310
|
+
answered = false;
|
|
311
|
+
tables.set(file.fileName, topLevel(file));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
293
314
|
const ids = new Set<string>();
|
|
294
|
-
for (const exported of
|
|
315
|
+
for (const exported of table) {
|
|
295
316
|
for (const target of chains.chainOf(exported)) {
|
|
317
|
+
answered &&= !target.guessed;
|
|
296
318
|
ids.add(target.id);
|
|
297
319
|
}
|
|
298
320
|
}
|
|
299
321
|
tables.set(file.fileName, [...ids]);
|
|
300
322
|
});
|
|
301
|
-
return tables;
|
|
323
|
+
return { tables, answered };
|
|
302
324
|
}
|
|
303
325
|
|
|
304
326
|
/** The kinds of container whose export table, not its members' visibility, says what it publishes. */
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The serialization-contract class: the
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
2
|
+
* The serialization-contract class: the data members and `toJSON` of every class,
|
|
3
|
+
* interface and object type a value's static type names, and of every such type its
|
|
4
|
+
* data members' types reach, where the value reaches `JSON.stringify` or a call
|
|
5
|
+
* `ts.serializers` names, and those and `toString` where it is passed to an `unknown`
|
|
6
|
+
* or `any` parameter of a function outside the analysis. A function of the project
|
|
7
|
+
* handing such a parameter of its own to a destination is one, holding back what that
|
|
8
|
+
* destination does, to a fixpoint. A function invoked through `Function.prototype.call`,
|
|
9
|
+
* `apply` or `bind` is the callee, given the arguments those methods pass it.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import {
|
|
@@ -56,8 +57,9 @@ import {
|
|
|
56
57
|
import type { DetectorInput, Evidence } from "./exempt.ts";
|
|
57
58
|
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
58
59
|
import { renderPosition } from "./position.ts";
|
|
60
|
+
import { must } from "./query.ts";
|
|
59
61
|
import type { ProjectView } from "./session.ts";
|
|
60
|
-
import { valueReach } from "./value-reach.ts";
|
|
62
|
+
import { valueReach, type Reached } from "./value-reach.ts";
|
|
61
63
|
|
|
62
64
|
/** The serializer every run reads, whatever the configuration names. */
|
|
63
65
|
const JSON_STRINGIFY: DeclarationEntry = { shape: "global", global: "JSON.stringify" };
|
|
@@ -65,6 +67,9 @@ const JSON_STRINGIFY: DeclarationEntry = { shape: "global", global: "JSON.string
|
|
|
65
67
|
/** The methods a serializer and a formatter resolve by name on a value that left the analysis. */
|
|
66
68
|
const CONVERSIONS: ReadonlySet<string> = new Set(["toJSON", "toString"]);
|
|
67
69
|
|
|
70
|
+
/** The method a serializer calls on a value that carries one, wherever the value goes. */
|
|
71
|
+
const SERIALIZED_CONVERSION = "toJSON";
|
|
72
|
+
|
|
68
73
|
/** The parameter types that keep nothing of the value they are given. */
|
|
69
74
|
const ERASED = TypeFlags.Any | TypeFlags.Unknown;
|
|
70
75
|
|
|
@@ -312,12 +317,13 @@ function erasedParameters<Brand>(
|
|
|
312
317
|
if (!parameter.rest) {
|
|
313
318
|
return (type.flags & ERASED) !== 0;
|
|
314
319
|
}
|
|
315
|
-
const [element] =
|
|
320
|
+
const [element] = must(project.queries.typeArguments(type));
|
|
316
321
|
return element !== undefined && (element.flags & ERASED) !== 0;
|
|
317
322
|
});
|
|
318
323
|
const bySymbol = new Map<number, Parameter>();
|
|
319
324
|
project
|
|
320
325
|
.symbolsAt(erased.map((parameter) => project.handle(parameter.name)))
|
|
326
|
+
.map(must)
|
|
321
327
|
.forEach((symbol, index) => {
|
|
322
328
|
const parameter = erased[index];
|
|
323
329
|
if (symbol !== undefined && parameter !== undefined) {
|
|
@@ -343,11 +349,11 @@ function handedOn<Brand>(
|
|
|
343
349
|
),
|
|
344
350
|
);
|
|
345
351
|
const plain = candidates.filter((one) => !one.value.shorthand);
|
|
346
|
-
const resolved = project.symbolsAt(plain.map((one) => project.handle(one.value.name)));
|
|
352
|
+
const resolved = project.symbolsAt(plain.map((one) => project.handle(one.value.name))).map(must);
|
|
347
353
|
const symbolOf = new Map(plain.map((one, index) => [one, resolved[index]]));
|
|
348
354
|
return candidates.flatMap((one) => {
|
|
349
355
|
const symbol = one.value.shorthand
|
|
350
|
-
? project.shorthandValueAt(project.handle(one.value.name))
|
|
356
|
+
? must(project.shorthandValueAt(project.handle(one.value.name)))
|
|
351
357
|
: symbolOf.get(one);
|
|
352
358
|
const parameter = symbol === undefined ? undefined : erased.get(symbol.id);
|
|
353
359
|
return parameter === undefined ? [] : [{ parameter, site: one.site, argument: one.argument }];
|
|
@@ -378,8 +384,8 @@ function destinations<Brand>(
|
|
|
378
384
|
walked: Walked,
|
|
379
385
|
): Destinations {
|
|
380
386
|
const { project, held } = input;
|
|
381
|
-
const
|
|
382
|
-
const own =
|
|
387
|
+
const queries = project.queries;
|
|
388
|
+
const own = project.ownPaths();
|
|
383
389
|
const named = resolveEntries(project, held, [JSON_STRINGIFY, ...input.ts.serializers]).filter(
|
|
384
390
|
namesADeclaration,
|
|
385
391
|
);
|
|
@@ -418,7 +424,7 @@ function destinations<Brand>(
|
|
|
418
424
|
form !== undefined &&
|
|
419
425
|
method !== undefined &&
|
|
420
426
|
outside(method) &&
|
|
421
|
-
FUNCTION_INTERFACES.has(
|
|
427
|
+
FUNCTION_INTERFACES.has(must(project.queries.parentOf(method))?.name ?? "")
|
|
422
428
|
) {
|
|
423
429
|
return form.invocation;
|
|
424
430
|
}
|
|
@@ -435,16 +441,16 @@ function destinations<Brand>(
|
|
|
435
441
|
const invoked = invocation(call);
|
|
436
442
|
// Through a method of the prototype the resolved signature is the method's, so
|
|
437
443
|
// the parameters are read off the callee's own signatures, any one erasing.
|
|
438
|
-
const type = invoked.indirect ?
|
|
444
|
+
const type = invoked.indirect ? must(queries.typeAt(invoked.callee)) : undefined;
|
|
439
445
|
const signatures = invoked.indirect
|
|
440
446
|
? type === undefined
|
|
441
447
|
? []
|
|
442
|
-
:
|
|
443
|
-
: [
|
|
448
|
+
: must(queries.signaturesOf(type, SignatureKind.Call))
|
|
449
|
+
: [must(queries.resolvedSignature(call))].filter((one) => one !== undefined);
|
|
444
450
|
indexes = new Set(
|
|
445
451
|
invoked.arguments.flatMap((_unused, index) =>
|
|
446
452
|
signatures.some((signature) => {
|
|
447
|
-
const parameter =
|
|
453
|
+
const parameter = must(queries.parameterType(signature, index));
|
|
448
454
|
return parameter !== undefined && (parameter.flags & ERASED) !== 0;
|
|
449
455
|
})
|
|
450
456
|
? [index]
|
|
@@ -521,10 +527,12 @@ function destinations<Brand>(
|
|
|
521
527
|
return { invocation, at, mayReceive };
|
|
522
528
|
}
|
|
523
529
|
|
|
524
|
-
/** One argument whose value carries classes of the target into a destination. */
|
|
530
|
+
/** One argument whose value carries classes and object types of the target into a destination. */
|
|
525
531
|
interface Flow {
|
|
526
532
|
readonly site: Node;
|
|
527
533
|
readonly classes: ReadonlySet<string>;
|
|
534
|
+
/** The members of the interfaces and object types the value carries. */
|
|
535
|
+
readonly members: readonly TSSymbol[];
|
|
528
536
|
readonly to: Destination;
|
|
529
537
|
}
|
|
530
538
|
|
|
@@ -554,31 +562,34 @@ function flowsOf<Brand>(
|
|
|
554
562
|
);
|
|
555
563
|
return arguments_.flatMap((one) => {
|
|
556
564
|
const type = typeOf.get(one.value);
|
|
557
|
-
let
|
|
565
|
+
let reached: Reached = { classes: new Set<string>(), members: [] };
|
|
558
566
|
if (one.value.kind === SyntaxKind.ThisKeyword) {
|
|
559
|
-
|
|
567
|
+
reached = {
|
|
568
|
+
classes: new Set(one.site.self === undefined ? [] : [one.site.self]),
|
|
569
|
+
members: [],
|
|
570
|
+
};
|
|
560
571
|
} else if (type !== undefined) {
|
|
561
|
-
|
|
572
|
+
reached = reach.reachedFrom(type);
|
|
562
573
|
}
|
|
563
|
-
const
|
|
564
|
-
|
|
574
|
+
const carries = reached.classes.size > 0 || reached.members.length > 0;
|
|
575
|
+
const destination = carries ? to.at(one.site.call, one.argument) : undefined;
|
|
576
|
+
return destination === undefined
|
|
577
|
+
? []
|
|
578
|
+
: [{ site: one.value, classes: reached.classes, members: reached.members, to: destination }];
|
|
565
579
|
});
|
|
566
580
|
}
|
|
567
581
|
|
|
568
582
|
/**
|
|
569
|
-
* The serialization-contract detector: each
|
|
570
|
-
*
|
|
571
|
-
* analysis, recorded at the argument with
|
|
583
|
+
* The serialization-contract detector: each data member and `toJSON` of each class,
|
|
584
|
+
* interface and object type a value that reaches a destination carries, and `toString`
|
|
585
|
+
* as well where the destination is outside the analysis, recorded at the argument with
|
|
586
|
+
* the callee named.
|
|
572
587
|
*/
|
|
573
588
|
export function serializationContract<Brand>(input: DetectorInput<Brand>): readonly Evidence[] {
|
|
574
589
|
const { project, held, targetRoot } = input;
|
|
575
590
|
const walked = walkProject(project, held);
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
held,
|
|
579
|
-
walked.sites,
|
|
580
|
-
destinations(input, aliasChains(project, held), walked),
|
|
581
|
-
);
|
|
591
|
+
const chains = aliasChains(project, held);
|
|
592
|
+
const flows = flowsOf(project, held, walked.sites, destinations(input, chains, walked));
|
|
582
593
|
if (flows.length === 0) {
|
|
583
594
|
return [];
|
|
584
595
|
}
|
|
@@ -589,22 +600,30 @@ export function serializationContract<Brand>(input: DetectorInput<Brand>): reado
|
|
|
589
600
|
"instance",
|
|
590
601
|
);
|
|
591
602
|
const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
|
|
603
|
+
// A destination holds back a data member and `toJSON`, and `toString` as well where
|
|
604
|
+
// the value left the analysis.
|
|
605
|
+
const holdsBack = (flags: SymbolFlags, name: string, to: Destination): boolean =>
|
|
606
|
+
(flags & SymbolFlags.Property) !== 0 ||
|
|
607
|
+
((flags & SymbolFlags.Method) !== 0 &&
|
|
608
|
+
(name === SERIALIZED_CONVERSION || (to.reach === "conversions" && CONVERSIONS.has(name))));
|
|
592
609
|
const found: Evidence[] = [];
|
|
593
610
|
for (const flow of flows) {
|
|
594
611
|
const site = renderPosition(flow.site.getSourceFile(), targetRoot, flow.site.getStart());
|
|
595
612
|
const detail = `passed to ${flow.to.callee}`;
|
|
596
613
|
for (const member of [...flow.classes].flatMap((id) => members.get(id) ?? [])) {
|
|
597
614
|
const symbol = byId.get(member.id);
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
flow.to.reach === "conversions" &&
|
|
601
|
-
(member.flags & SymbolFlags.Method) !== 0 &&
|
|
602
|
-
symbol !== undefined &&
|
|
603
|
-
CONVERSIONS.has(memberComponent(symbol, byId) ?? "");
|
|
604
|
-
if (isProperty || converts) {
|
|
615
|
+
const name = symbol === undefined ? "" : (memberComponent(symbol, byId) ?? "");
|
|
616
|
+
if (holdsBack(member.flags, name, flow.to)) {
|
|
605
617
|
found.push({ id: member.id, detail, site });
|
|
606
618
|
}
|
|
607
619
|
}
|
|
620
|
+
for (const member of flow.members) {
|
|
621
|
+
if (holdsBack(member.flags, member.name, flow.to)) {
|
|
622
|
+
for (const id of chains.declarationsOf(member)) {
|
|
623
|
+
found.push({ id, detail, site });
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
608
627
|
}
|
|
609
628
|
return found;
|
|
610
629
|
}
|