@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
|
@@ -1,14 +1,14 @@
|
|
|
1
|
+
import { jsonChunks } from "../json-chunks.js";
|
|
1
2
|
import { KINDS } from "../kinds.js";
|
|
2
3
|
import { lineHashes, symbolFingerprint } from "./fingerprint.js";
|
|
3
4
|
import { RenderError } from "./reporter.js";
|
|
4
|
-
import { RULE_TEXTS } from "./rules.js";
|
|
5
5
|
import { utf8Bytes } from "./sha256.js";
|
|
6
6
|
const SCHEMA = "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json";
|
|
7
7
|
const VERSION = "2.1.0";
|
|
8
8
|
/** The unit a finding's column counts. */
|
|
9
9
|
const COLUMN_KIND = "utf16CodeUnits";
|
|
10
10
|
const URI_BASE_ID = "%SRCROOT%";
|
|
11
|
-
const ROOT_DESCRIPTION = "The target root
|
|
11
|
+
const ROOT_DESCRIPTION = "The target root.";
|
|
12
12
|
/** The most related locations one result carries; the report stays complete beyond them. */
|
|
13
13
|
const MAX_RELATED = 100;
|
|
14
14
|
const INDENT = 2;
|
|
@@ -32,23 +32,23 @@ function firstSentence(text) {
|
|
|
32
32
|
}
|
|
33
33
|
return text;
|
|
34
34
|
}
|
|
35
|
+
/** Every live kind whose languages include one of the run's, in code order. */
|
|
36
|
+
function kindsOf(languages) {
|
|
37
|
+
return [...KINDS.values()].filter((kind) => kind.languages.some((language) => languages.includes(language)));
|
|
38
|
+
}
|
|
35
39
|
/**
|
|
36
|
-
* One rule per live kind of
|
|
40
|
+
* One rule per live kind of the run's languages, in code order, whether or not a result
|
|
37
41
|
* names it, so a rule's index is the same in every run and under every configuration.
|
|
38
42
|
*/
|
|
39
|
-
function rules() {
|
|
40
|
-
return
|
|
41
|
-
const kind = KINDS.get(code);
|
|
42
|
-
if (kind === undefined) {
|
|
43
|
-
throw new RenderError(`the rule ${code} names no live kind`);
|
|
44
|
-
}
|
|
43
|
+
function rules(kinds) {
|
|
44
|
+
return kinds.map((kind) => {
|
|
45
45
|
return {
|
|
46
|
-
id: code,
|
|
46
|
+
id: kind.code,
|
|
47
47
|
name: kind.name,
|
|
48
|
-
shortDescription: { text: firstSentence(
|
|
49
|
-
fullDescription: { text:
|
|
48
|
+
shortDescription: { text: firstSentence(kind.rule) },
|
|
49
|
+
fullDescription: { text: kind.rule },
|
|
50
50
|
help: {
|
|
51
|
-
text:
|
|
51
|
+
text: kind.precondition === undefined ? kind.rule : `${kind.rule}\n\n${kind.precondition}`,
|
|
52
52
|
},
|
|
53
53
|
defaultConfiguration: { level: LEVEL[kind.defaultSeverity] },
|
|
54
54
|
properties: {
|
|
@@ -120,6 +120,11 @@ function relatedOf(found) {
|
|
|
120
120
|
for (const at of found.details.write_positions ?? []) {
|
|
121
121
|
related.push({ label: "write", at, endLine: at.end_line });
|
|
122
122
|
}
|
|
123
|
+
for (const one of found.component.members ?? []) {
|
|
124
|
+
if (one.ref !== found.symbol.ref) {
|
|
125
|
+
related.push({ label: "member", at: one.position, endLine: one.position.end_line });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
123
128
|
return related.slice(0, MAX_RELATED);
|
|
124
129
|
}
|
|
125
130
|
/** The finding's message, then a link to each related location, since only a linked one shows. */
|
|
@@ -202,8 +207,9 @@ function staleResult(stale, index, hashOf) {
|
|
|
202
207
|
* rendering, because a line fingerprint computed from other bytes opens a second alert.
|
|
203
208
|
*/
|
|
204
209
|
export function sarif(report, options) {
|
|
205
|
-
const
|
|
206
|
-
const
|
|
210
|
+
const kinds = kindsOf(report.analyzer.languages);
|
|
211
|
+
const ruleList = rules(kinds);
|
|
212
|
+
const index = new Map(kinds.map((kind, at) => [kind.code, at]));
|
|
207
213
|
const hashOf = lineHashCache(options.readSource);
|
|
208
214
|
const results = [
|
|
209
215
|
...report.findings.map((found) => findingResult(found, index, hashOf)),
|
|
@@ -231,5 +237,5 @@ export function sarif(report, options) {
|
|
|
231
237
|
},
|
|
232
238
|
],
|
|
233
239
|
};
|
|
234
|
-
return
|
|
240
|
+
return jsonChunks(log, INDENT);
|
|
235
241
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* and `println`. Any other function, `define`, `template` and `block` are refused at parse,
|
|
7
7
|
* and a member the document does not carry fails the rendering.
|
|
8
8
|
*/
|
|
9
|
+
import { CHUNK_LENGTH } from "../json-chunks.js";
|
|
9
10
|
import { RenderError } from "./reporter.js";
|
|
10
11
|
import { utf8Bytes } from "./sha256.js";
|
|
11
12
|
/** A template that does not parse, which refuses the invocation before any analysis. */
|
|
@@ -49,7 +50,10 @@ const ESCAPES = {
|
|
|
49
50
|
/** The number of hexadecimal digits each numeric escape takes. */
|
|
50
51
|
const ESCAPE_WIDTHS = { x: 2, u: 4, U: 8 };
|
|
51
52
|
const IDENTIFIER = /[A-Za-z_][A-Za-z0-9_]*/uy;
|
|
52
|
-
|
|
53
|
+
/** A number as the lexer reads one, before the subset decides whether it is a decimal integer. */
|
|
54
|
+
const NUMBER = /[+-]?[0-9][0-9A-Za-z_.]*/uy;
|
|
55
|
+
/** The one number form the subset admits: a decimal integer with no leading zero. */
|
|
56
|
+
const DECIMAL_INTEGER = /^[+-]?(?:0|[1-9][0-9]*)$/u;
|
|
53
57
|
function lineOf(source, at) {
|
|
54
58
|
let line = 1;
|
|
55
59
|
for (let i = 0; i < at; i += 1) {
|
|
@@ -153,6 +157,9 @@ function lexAction(source, at, line) {
|
|
|
153
157
|
NUMBER.lastIndex = i;
|
|
154
158
|
const number = NUMBER.exec(source)?.[0];
|
|
155
159
|
if (number !== undefined) {
|
|
160
|
+
if (!DECIMAL_INTEGER.test(number)) {
|
|
161
|
+
throw new TemplateError(`line ${String(line)}: ${JSON.stringify(number)} is not a decimal integer`);
|
|
162
|
+
}
|
|
156
163
|
tokens.push({ t: "lit", value: Number(number) });
|
|
157
164
|
i += number.length;
|
|
158
165
|
continue;
|
|
@@ -530,8 +537,61 @@ function compareBasic(name, a, b) {
|
|
|
530
537
|
if (kind === "boolean") {
|
|
531
538
|
return Number.NaN;
|
|
532
539
|
}
|
|
540
|
+
if (kind === "string") {
|
|
541
|
+
return byCodePoints(a, b);
|
|
542
|
+
}
|
|
533
543
|
return a < b ? -1 : 1;
|
|
534
544
|
}
|
|
545
|
+
/**
|
|
546
|
+
* Two strings ordered by the bytes of their UTF-8 encodings, which is the order of their
|
|
547
|
+
* code points: the order of their UTF-16 code units differs above U+FFFF.
|
|
548
|
+
*/
|
|
549
|
+
function byCodePoints(a, b) {
|
|
550
|
+
let at = 0;
|
|
551
|
+
while (at < a.length && at < b.length) {
|
|
552
|
+
const x = a.codePointAt(at) ?? 0;
|
|
553
|
+
const y = b.codePointAt(at) ?? 0;
|
|
554
|
+
if (x !== y) {
|
|
555
|
+
return x < y ? -1 : 1;
|
|
556
|
+
}
|
|
557
|
+
at += x > 0xffff ? 2 : 1;
|
|
558
|
+
}
|
|
559
|
+
return a.length - b.length;
|
|
560
|
+
}
|
|
561
|
+
/** The escapes `%q` writes for the control characters that have one of their own. */
|
|
562
|
+
const QUOTED_ESCAPES = {
|
|
563
|
+
'"': '\\"',
|
|
564
|
+
"\\": "\\\\",
|
|
565
|
+
"\u0007": "\\a",
|
|
566
|
+
"\b": "\\b",
|
|
567
|
+
"\f": "\\f",
|
|
568
|
+
"\n": "\\n",
|
|
569
|
+
"\r": "\\r",
|
|
570
|
+
"\t": "\\t",
|
|
571
|
+
"\v": "\\v",
|
|
572
|
+
};
|
|
573
|
+
/**
|
|
574
|
+
* The text `%q` writes: double quotes around the text, a quote and a backslash escaped,
|
|
575
|
+
* each control character with an escape of its own written as it, every other character
|
|
576
|
+
* below U+0020 and U+007F written as `\x` and two lowercase hexadecimal digits, and every
|
|
577
|
+
* other character as itself.
|
|
578
|
+
*/
|
|
579
|
+
function goQuoted(text) {
|
|
580
|
+
let out = '"';
|
|
581
|
+
for (const char of text) {
|
|
582
|
+
const code = char.codePointAt(0) ?? 0;
|
|
583
|
+
if (Object.hasOwn(QUOTED_ESCAPES, char)) {
|
|
584
|
+
out += QUOTED_ESCAPES[char] ?? "";
|
|
585
|
+
}
|
|
586
|
+
else if (code < 0x20 || code === 0x7f) {
|
|
587
|
+
out += `\\x${code.toString(16).padStart(2, "0")}`;
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
out += char;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return `${out}"`;
|
|
594
|
+
}
|
|
535
595
|
/** `printf`'s verbs, each over one operand. */
|
|
536
596
|
function printf(format, args) {
|
|
537
597
|
let out = "";
|
|
@@ -556,10 +616,10 @@ function printf(format, args) {
|
|
|
556
616
|
switch (verb) {
|
|
557
617
|
case "v":
|
|
558
618
|
case "s":
|
|
559
|
-
out += formatted(arg,
|
|
619
|
+
out += formatted(arg, true);
|
|
560
620
|
break;
|
|
561
621
|
case "q":
|
|
562
|
-
out +=
|
|
622
|
+
out += goQuoted(formatted(arg, true));
|
|
563
623
|
break;
|
|
564
624
|
case "d":
|
|
565
625
|
if (typeof arg !== "number" || !Number.isInteger(arg)) {
|
|
@@ -659,11 +719,11 @@ function call(name, args) {
|
|
|
659
719
|
return args
|
|
660
720
|
.map((arg, i) => {
|
|
661
721
|
const spaced = i > 0 && typeof arg !== "string" && typeof args[i - 1] !== "string";
|
|
662
|
-
return `${spaced ? " " : ""}${formatted(arg,
|
|
722
|
+
return `${spaced ? " " : ""}${formatted(arg, true)}`;
|
|
663
723
|
})
|
|
664
724
|
.join("");
|
|
665
725
|
case "println":
|
|
666
|
-
return `${args.map((arg) => formatted(arg,
|
|
726
|
+
return `${args.map((arg) => formatted(arg, true)).join(" ")}\n`;
|
|
667
727
|
case "printf":
|
|
668
728
|
if (typeof first !== "string") {
|
|
669
729
|
throw new RenderError("printf takes a format string first");
|
|
@@ -676,10 +736,22 @@ function call(name, args) {
|
|
|
676
736
|
/** One execution of a template over a document. */
|
|
677
737
|
class Execution {
|
|
678
738
|
scopes;
|
|
679
|
-
|
|
739
|
+
/** The rendering so far, in pieces of about {@link CHUNK_LENGTH}, the last still growing. */
|
|
740
|
+
pieces = [""];
|
|
680
741
|
constructor(root) {
|
|
681
742
|
this.scopes = [new Map([["$", root]])];
|
|
682
743
|
}
|
|
744
|
+
write(text) {
|
|
745
|
+
const last = this.pieces.length - 1;
|
|
746
|
+
const held = (this.pieces[last] ?? "") + text;
|
|
747
|
+
if (held.length >= CHUNK_LENGTH) {
|
|
748
|
+
this.pieces[last] = held;
|
|
749
|
+
this.pieces.push("");
|
|
750
|
+
}
|
|
751
|
+
else {
|
|
752
|
+
this.pieces[last] = held;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
683
755
|
lookup(name) {
|
|
684
756
|
for (let i = this.scopes.length - 1; i >= 0; i -= 1) {
|
|
685
757
|
const scope = this.scopes[i];
|
|
@@ -783,12 +855,12 @@ class Execution {
|
|
|
783
855
|
for (const node of nodes) {
|
|
784
856
|
switch (node.k) {
|
|
785
857
|
case "text":
|
|
786
|
-
this.
|
|
858
|
+
this.write(node.text);
|
|
787
859
|
break;
|
|
788
860
|
case "action": {
|
|
789
861
|
const value = this.pipeline(node.pipe, dot, true);
|
|
790
862
|
if (node.pipe.vars.length === 0) {
|
|
791
|
-
this.
|
|
863
|
+
this.write(formatted(value, false));
|
|
792
864
|
}
|
|
793
865
|
break;
|
|
794
866
|
}
|
|
@@ -866,12 +938,40 @@ class Execution {
|
|
|
866
938
|
}
|
|
867
939
|
}
|
|
868
940
|
/**
|
|
869
|
-
*
|
|
870
|
-
*
|
|
941
|
+
* One document as the JSON value it encodes: what `JSON.stringify` would write for it,
|
|
942
|
+
* read back, without writing it. A member holding nothing is left out, and an array
|
|
943
|
+
* element holding nothing, like a number that is not finite, is null.
|
|
944
|
+
*/
|
|
945
|
+
function plainOf(value) {
|
|
946
|
+
if (Array.isArray(value)) {
|
|
947
|
+
return value.map((element) => plainOf(element) ?? null);
|
|
948
|
+
}
|
|
949
|
+
if (typeof value === "object" && value !== null) {
|
|
950
|
+
const held = {};
|
|
951
|
+
for (const [key, member] of Object.entries(value)) {
|
|
952
|
+
const plain = plainOf(member);
|
|
953
|
+
if (plain !== undefined) {
|
|
954
|
+
held[key] = plain;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return held;
|
|
958
|
+
}
|
|
959
|
+
if (typeof value === "number") {
|
|
960
|
+
return Number.isFinite(value) ? value : null;
|
|
961
|
+
}
|
|
962
|
+
if (typeof value === "string" || typeof value === "boolean" || value === null) {
|
|
963
|
+
return value;
|
|
964
|
+
}
|
|
965
|
+
return undefined;
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* The template's rendering of one document, in pieces of about {@link CHUNK_LENGTH}.
|
|
969
|
+
* The rendering is complete before it is answered, so a template that fails partway
|
|
970
|
+
* renders nothing, and the failure names what it could not evaluate.
|
|
871
971
|
*/
|
|
872
972
|
export function renderTemplate(template, document) {
|
|
873
|
-
const root =
|
|
973
|
+
const root = plainOf(document) ?? null;
|
|
874
974
|
const execution = new Execution(root);
|
|
875
975
|
execution.run(template.nodes, root);
|
|
876
|
-
return execution.
|
|
976
|
+
return execution.pieces.filter((piece) => piece !== "");
|
|
877
977
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { coalesced } from "../json-chunks.js";
|
|
1
2
|
/** The kind token and the confidence a stale suppression is rendered with, both fixed. */
|
|
2
3
|
const STALE_KIND = "suppression";
|
|
3
4
|
const STALE_CONFIDENCE = "certain";
|
|
@@ -24,11 +25,12 @@ function summary(totals) {
|
|
|
24
25
|
* summary is deliberately not in that shape, so a filter for finding lines yields exactly
|
|
25
26
|
* them. Nothing else is written: no timestamp, duration or host detail.
|
|
26
27
|
*/
|
|
27
|
-
export const text = (report) => {
|
|
28
|
-
const
|
|
28
|
+
export const text = (report) => coalesced((function* lines() {
|
|
29
|
+
for (const found of report.findings) {
|
|
30
|
+
yield `${found.position.path}:${String(found.position.line)}:${String(found.position.column)}: ${found.symbol.kind} ${found.symbol.name}: ${found.message} [${found.confidence}] (${found.code})\n`;
|
|
31
|
+
}
|
|
29
32
|
for (const stale of report.stale_suppressions) {
|
|
30
|
-
|
|
33
|
+
yield `${stale.position.path}:${String(stale.position.line)}:${String(stale.position.column)}: ${STALE_KIND} ${stale.symbol}: ${stale.message} [${STALE_CONFIDENCE}] (${stale.code})\n`;
|
|
31
34
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
};
|
|
35
|
+
yield `${summary(report.totals)}\n`;
|
|
36
|
+
})());
|
package/dist/src/roots.js
CHANGED
|
@@ -12,6 +12,7 @@ import { entryPoints } from "./entry-points.js";
|
|
|
12
12
|
import { globExpression } from "./glob.js";
|
|
13
13
|
import { nodeKey } from "./inventory.js";
|
|
14
14
|
import { byPosition } from "./position.js";
|
|
15
|
+
import { UNANSWERED } from "./query.js";
|
|
15
16
|
import { sourceFilesOf } from "./source-files.js";
|
|
16
17
|
/** Whether a configured string carries either special character. */
|
|
17
18
|
function isPattern(text) {
|
|
@@ -142,7 +143,7 @@ export function roots(project, held, targetRoot, options) {
|
|
|
142
143
|
for (const point of entryPoints(project, files, options.testFiles)) {
|
|
143
144
|
enter(point.file, point.rule, point.source, point.exports);
|
|
144
145
|
}
|
|
145
|
-
const exportsOf = exportTables(project, held, [
|
|
146
|
+
const { tables: exportsOf, answered } = exportTables(project, held, [
|
|
146
147
|
...entered.filter((entry) => entry.exports).map((entry) => entry.file),
|
|
147
148
|
...published,
|
|
148
149
|
]);
|
|
@@ -171,41 +172,56 @@ export function roots(project, held, targetRoot, options) {
|
|
|
171
172
|
const site = left === undefined || right === undefined ? 0 : byPosition(left, right);
|
|
172
173
|
return site || compare(a.kind, b.kind) || compare(a.source, b.source);
|
|
173
174
|
});
|
|
174
|
-
return {
|
|
175
|
+
return {
|
|
176
|
+
configFile: project.configFile,
|
|
177
|
+
liveUnderReachability: ordered,
|
|
178
|
+
unmatched,
|
|
179
|
+
unanswered: answered ? [] : held.symbols.map((symbol) => symbol.id),
|
|
180
|
+
};
|
|
175
181
|
}
|
|
176
182
|
/**
|
|
177
183
|
* Every declaration each file's module exports, by the file's name: the declaration
|
|
178
184
|
* each name of the export table is, and every link of a re-export's chain. A name
|
|
179
|
-
* whose declaration is outside the project's own files
|
|
180
|
-
* and
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
* its export table, star re-exports resolved into it. A file that is no module
|
|
184
|
-
* exports nothing.
|
|
185
|
+
* whose declaration is outside the project's own files names nothing here. One batch
|
|
186
|
+
* resolves every file to its module and one request per module reads its export
|
|
187
|
+
* table, star re-exports resolved into it. A file that is no module exports nothing.
|
|
188
|
+
* `answered` is false where the checker left a table or a link of a chain unanswered.
|
|
185
189
|
*/
|
|
186
190
|
function exportTables(project, held, wanted) {
|
|
187
191
|
const unique = [...new Map(wanted.map((file) => [file.fileName, file])).values()];
|
|
188
192
|
const tables = new Map();
|
|
193
|
+
let answered = true;
|
|
189
194
|
if (unique.length === 0) {
|
|
190
|
-
return tables;
|
|
195
|
+
return { tables, answered };
|
|
191
196
|
}
|
|
192
197
|
const modules = project.symbolsAt(unique.map((file) => project.handle(file)));
|
|
193
198
|
const chains = aliasChains(project, held);
|
|
199
|
+
const topLevel = (file) => {
|
|
200
|
+
const fileId = held.declarations.get(nodeKey(file, file));
|
|
201
|
+
return held.symbols.filter((symbol) => symbol.parent === fileId).map((symbol) => symbol.id);
|
|
202
|
+
};
|
|
194
203
|
unique.forEach((file, index) => {
|
|
195
204
|
const module = modules[index];
|
|
196
205
|
if (module === undefined) {
|
|
197
206
|
tables.set(file.fileName, []);
|
|
198
207
|
return;
|
|
199
208
|
}
|
|
209
|
+
const table = module === UNANSWERED ? UNANSWERED : project.queries.exportsOfModule(module);
|
|
210
|
+
if (table === UNANSWERED) {
|
|
211
|
+
answered = false;
|
|
212
|
+
tables.set(file.fileName, topLevel(file));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
200
215
|
const ids = new Set();
|
|
201
|
-
for (const exported of
|
|
216
|
+
for (const exported of table) {
|
|
202
217
|
for (const target of chains.chainOf(exported)) {
|
|
218
|
+
answered &&= !target.guessed;
|
|
203
219
|
ids.add(target.id);
|
|
204
220
|
}
|
|
205
221
|
}
|
|
206
222
|
tables.set(file.fileName, [...ids]);
|
|
207
223
|
});
|
|
208
|
-
return tables;
|
|
224
|
+
return { tables, answered };
|
|
209
225
|
}
|
|
210
226
|
/** The kinds of container whose export table, not its members' visibility, says what it publishes. */
|
|
211
227
|
const EXPORTING_CONTAINERS = new Set(["file", "namespace"]);
|
|
@@ -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
|
import { isArrayLiteralExpression, isArrowFunction, isCallExpression, isClassDeclaration, isClassExpression, isClassStaticBlockDeclaration, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isSetAccessorDeclaration, isSpreadElement, isStringLiteral, isVariableDeclaration, ModifierFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
11
12
|
import { SignatureKind, SymbolFlags, TypeFlags, } from "@typescript/native/unstable/sync";
|
|
@@ -15,11 +16,14 @@ import { classMembers, memberComponent, typesAt } from "./class-members.js";
|
|
|
15
16
|
import { names, namesADeclaration, resolveEntries, spelledEntry, } from "./configured-declarations.js";
|
|
16
17
|
import { nodeKey } from "./inventory.js";
|
|
17
18
|
import { renderPosition } from "./position.js";
|
|
19
|
+
import { must } from "./query.js";
|
|
18
20
|
import { valueReach } from "./value-reach.js";
|
|
19
21
|
/** The serializer every run reads, whatever the configuration names. */
|
|
20
22
|
const JSON_STRINGIFY = { shape: "global", global: "JSON.stringify" };
|
|
21
23
|
/** The methods a serializer and a formatter resolve by name on a value that left the analysis. */
|
|
22
24
|
const CONVERSIONS = new Set(["toJSON", "toString"]);
|
|
25
|
+
/** The method a serializer calls on a value that carries one, wherever the value goes. */
|
|
26
|
+
const SERIALIZED_CONVERSION = "toJSON";
|
|
23
27
|
/** The parameter types that keep nothing of the value they are given. */
|
|
24
28
|
const ERASED = TypeFlags.Any | TypeFlags.Unknown;
|
|
25
29
|
/** The wider of two reaches, where either may be absent. */
|
|
@@ -182,12 +186,13 @@ function erasedParameters(project, parameters) {
|
|
|
182
186
|
if (!parameter.rest) {
|
|
183
187
|
return (type.flags & ERASED) !== 0;
|
|
184
188
|
}
|
|
185
|
-
const [element] =
|
|
189
|
+
const [element] = must(project.queries.typeArguments(type));
|
|
186
190
|
return element !== undefined && (element.flags & ERASED) !== 0;
|
|
187
191
|
});
|
|
188
192
|
const bySymbol = new Map();
|
|
189
193
|
project
|
|
190
194
|
.symbolsAt(erased.map((parameter) => project.handle(parameter.name)))
|
|
195
|
+
.map(must)
|
|
191
196
|
.forEach((symbol, index) => {
|
|
192
197
|
const parameter = erased[index];
|
|
193
198
|
if (symbol !== undefined && parameter !== undefined) {
|
|
@@ -203,11 +208,11 @@ function handedOn(project, sites, erased, invocationOf) {
|
|
|
203
208
|
.filter((value) => spellings.has(value.name.getText()))
|
|
204
209
|
.map((value) => ({ site, argument: index, value }))));
|
|
205
210
|
const plain = candidates.filter((one) => !one.value.shorthand);
|
|
206
|
-
const resolved = project.symbolsAt(plain.map((one) => project.handle(one.value.name)));
|
|
211
|
+
const resolved = project.symbolsAt(plain.map((one) => project.handle(one.value.name))).map(must);
|
|
207
212
|
const symbolOf = new Map(plain.map((one, index) => [one, resolved[index]]));
|
|
208
213
|
return candidates.flatMap((one) => {
|
|
209
214
|
const symbol = one.value.shorthand
|
|
210
|
-
? project.shorthandValueAt(project.handle(one.value.name))
|
|
215
|
+
? must(project.shorthandValueAt(project.handle(one.value.name)))
|
|
211
216
|
: symbolOf.get(one);
|
|
212
217
|
const parameter = symbol === undefined ? undefined : erased.get(symbol.id);
|
|
213
218
|
return parameter === undefined ? [] : [{ parameter, site: one.site, argument: one.argument }];
|
|
@@ -223,8 +228,8 @@ function handedOn(project, sites, erased, invocationOf) {
|
|
|
223
228
|
*/
|
|
224
229
|
function destinations(input, chains, walked) {
|
|
225
230
|
const { project, held } = input;
|
|
226
|
-
const
|
|
227
|
-
const own =
|
|
231
|
+
const queries = project.queries;
|
|
232
|
+
const own = project.ownPaths();
|
|
228
233
|
const named = resolveEntries(project, held, [JSON_STRINGIFY, ...input.ts.serializers]).filter(namesADeclaration);
|
|
229
234
|
const forms = new Map();
|
|
230
235
|
for (const site of walked.sites) {
|
|
@@ -256,7 +261,7 @@ function destinations(input, chains, walked) {
|
|
|
256
261
|
if (form !== undefined &&
|
|
257
262
|
method !== undefined &&
|
|
258
263
|
outside(method) &&
|
|
259
|
-
FUNCTION_INTERFACES.has(
|
|
264
|
+
FUNCTION_INTERFACES.has(must(project.queries.parentOf(method))?.name ?? "")) {
|
|
260
265
|
return form.invocation;
|
|
261
266
|
}
|
|
262
267
|
return { callee: call.expression, arguments: call.arguments, indirect: false };
|
|
@@ -272,14 +277,14 @@ function destinations(input, chains, walked) {
|
|
|
272
277
|
const invoked = invocation(call);
|
|
273
278
|
// Through a method of the prototype the resolved signature is the method's, so
|
|
274
279
|
// the parameters are read off the callee's own signatures, any one erasing.
|
|
275
|
-
const type = invoked.indirect ?
|
|
280
|
+
const type = invoked.indirect ? must(queries.typeAt(invoked.callee)) : undefined;
|
|
276
281
|
const signatures = invoked.indirect
|
|
277
282
|
? type === undefined
|
|
278
283
|
? []
|
|
279
|
-
:
|
|
280
|
-
: [
|
|
284
|
+
: must(queries.signaturesOf(type, SignatureKind.Call))
|
|
285
|
+
: [must(queries.resolvedSignature(call))].filter((one) => one !== undefined);
|
|
281
286
|
indexes = new Set(invoked.arguments.flatMap((_unused, index) => signatures.some((signature) => {
|
|
282
|
-
const parameter =
|
|
287
|
+
const parameter = must(queries.parameterType(signature, index));
|
|
283
288
|
return parameter !== undefined && (parameter.flags & ERASED) !== 0;
|
|
284
289
|
})
|
|
285
290
|
? [index]
|
|
@@ -360,46 +365,62 @@ function flowsOf(project, held, sites, to) {
|
|
|
360
365
|
const typeOf = new Map(asked.map((one, index) => [one.value, types[index]]));
|
|
361
366
|
return arguments_.flatMap((one) => {
|
|
362
367
|
const type = typeOf.get(one.value);
|
|
363
|
-
let
|
|
368
|
+
let reached = { classes: new Set(), members: [] };
|
|
364
369
|
if (one.value.kind === SyntaxKind.ThisKeyword) {
|
|
365
|
-
|
|
370
|
+
reached = {
|
|
371
|
+
classes: new Set(one.site.self === undefined ? [] : [one.site.self]),
|
|
372
|
+
members: [],
|
|
373
|
+
};
|
|
366
374
|
}
|
|
367
375
|
else if (type !== undefined) {
|
|
368
|
-
|
|
376
|
+
reached = reach.reachedFrom(type);
|
|
369
377
|
}
|
|
370
|
-
const
|
|
371
|
-
|
|
378
|
+
const carries = reached.classes.size > 0 || reached.members.length > 0;
|
|
379
|
+
const destination = carries ? to.at(one.site.call, one.argument) : undefined;
|
|
380
|
+
return destination === undefined
|
|
381
|
+
? []
|
|
382
|
+
: [{ site: one.value, classes: reached.classes, members: reached.members, to: destination }];
|
|
372
383
|
});
|
|
373
384
|
}
|
|
374
385
|
/**
|
|
375
|
-
* The serialization-contract detector: each
|
|
376
|
-
*
|
|
377
|
-
* analysis, recorded at the argument with
|
|
386
|
+
* The serialization-contract detector: each data member and `toJSON` of each class,
|
|
387
|
+
* interface and object type a value that reaches a destination carries, and `toString`
|
|
388
|
+
* as well where the destination is outside the analysis, recorded at the argument with
|
|
389
|
+
* the callee named.
|
|
378
390
|
*/
|
|
379
391
|
export function serializationContract(input) {
|
|
380
392
|
const { project, held, targetRoot } = input;
|
|
381
393
|
const walked = walkProject(project, held);
|
|
382
|
-
const
|
|
394
|
+
const chains = aliasChains(project, held);
|
|
395
|
+
const flows = flowsOf(project, held, walked.sites, destinations(input, chains, walked));
|
|
383
396
|
if (flows.length === 0) {
|
|
384
397
|
return [];
|
|
385
398
|
}
|
|
386
399
|
const members = classMembers(project, held, new Set(flows.flatMap((flow) => [...flow.classes])), "instance");
|
|
387
400
|
const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
|
|
401
|
+
// A destination holds back a data member and `toJSON`, and `toString` as well where
|
|
402
|
+
// the value left the analysis.
|
|
403
|
+
const holdsBack = (flags, name, to) => (flags & SymbolFlags.Property) !== 0 ||
|
|
404
|
+
((flags & SymbolFlags.Method) !== 0 &&
|
|
405
|
+
(name === SERIALIZED_CONVERSION || (to.reach === "conversions" && CONVERSIONS.has(name))));
|
|
388
406
|
const found = [];
|
|
389
407
|
for (const flow of flows) {
|
|
390
408
|
const site = renderPosition(flow.site.getSourceFile(), targetRoot, flow.site.getStart());
|
|
391
409
|
const detail = `passed to ${flow.to.callee}`;
|
|
392
410
|
for (const member of [...flow.classes].flatMap((id) => members.get(id) ?? [])) {
|
|
393
411
|
const symbol = byId.get(member.id);
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
(member.flags & SymbolFlags.Method) !== 0 &&
|
|
397
|
-
symbol !== undefined &&
|
|
398
|
-
CONVERSIONS.has(memberComponent(symbol, byId) ?? "");
|
|
399
|
-
if (isProperty || converts) {
|
|
412
|
+
const name = symbol === undefined ? "" : (memberComponent(symbol, byId) ?? "");
|
|
413
|
+
if (holdsBack(member.flags, name, flow.to)) {
|
|
400
414
|
found.push({ id: member.id, detail, site });
|
|
401
415
|
}
|
|
402
416
|
}
|
|
417
|
+
for (const member of flow.members) {
|
|
418
|
+
if (holdsBack(member.flags, member.name, flow.to)) {
|
|
419
|
+
for (const id of chains.declarationsOf(member)) {
|
|
420
|
+
found.push({ id, detail, site });
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
403
424
|
}
|
|
404
425
|
return found;
|
|
405
426
|
}
|