@cplieger/deadset-ts 3.12.1 → 3.13.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 +2 -2
- package/dist/src/analysis.js +100 -27
- package/dist/src/components.js +4 -2
- package/dist/src/configured-declarations.js +36 -0
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/edges.js +4 -1
- package/dist/src/findings/completion.js +37 -8
- package/dist/src/findings/emitters.js +5 -5
- 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/intra-function-parts.js +643 -0
- package/dist/src/kinds.js +337 -35
- package/dist/src/reflective-lookup.js +35 -2
- package/dist/src/report.js +11 -2
- package/dist/src/reporters/sarif.js +20 -15
- package/dist/src/reporters/template.js +64 -5
- package/dist/src/serialization-contract.js +41 -22
- package/dist/src/signatures.js +32 -0
- package/dist/src/suppress-file.js +27 -6
- package/dist/src/suppress-inline.js +8 -1
- package/dist/src/suppress.js +27 -0
- package/dist/src/value-reach.js +33 -9
- package/dist/src/verbs/analyze.js +19 -11
- package/dist/src/verbs/describe.js +12 -8
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +167 -48
- package/src/components.ts +4 -2
- package/src/configured-declarations.ts +57 -1
- package/src/conformance-record.ts +2 -2
- package/src/edges.ts +4 -1
- 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 +9 -5
- 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/intra-function-parts.ts +871 -0
- package/src/kinds.ts +355 -41
- package/src/reflective-lookup.ts +42 -2
- package/src/report.ts +18 -2
- package/src/reporters/sarif.ts +24 -16
- package/src/reporters/template.ts +69 -6
- package/src/serialization-contract.ts +47 -30
- package/src/signatures.ts +55 -0
- package/src/suppress-file.ts +34 -6
- package/src/suppress-inline.ts +8 -1
- package/src/suppress.ts +31 -0
- package/src/value-reach.ts +53 -12
- package/src/verbs/analyze.ts +26 -11
- package/src/verbs/describe.ts +13 -9
- package/src/version.ts +1 -1
- package/dist/src/reporters/rules.js +0 -178
- package/src/reporters/rules.ts +0 -195
package/src/reporters/sarif.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { Confidence, Severity } from "../config.ts";
|
|
2
|
-
import { KINDS } from "../kinds.ts";
|
|
2
|
+
import { KINDS, type KindRow } from "../kinds.ts";
|
|
3
3
|
import type { Report, WireFinding, WireStaleSuppression } from "../report.ts";
|
|
4
4
|
import { lineHashes, symbolFingerprint } from "./fingerprint.ts";
|
|
5
5
|
import { RenderError, type RenderOptions } from "./reporter.ts";
|
|
6
|
-
import { RULE_TEXTS } from "./rules.ts";
|
|
7
6
|
import { utf8Bytes } from "./sha256.ts";
|
|
8
7
|
|
|
9
8
|
const SCHEMA =
|
|
@@ -12,7 +11,7 @@ const VERSION = "2.1.0";
|
|
|
12
11
|
/** The unit a finding's column counts. */
|
|
13
12
|
const COLUMN_KIND = "utf16CodeUnits";
|
|
14
13
|
const URI_BASE_ID = "%SRCROOT%";
|
|
15
|
-
const ROOT_DESCRIPTION = "The target root
|
|
14
|
+
const ROOT_DESCRIPTION = "The target root.";
|
|
16
15
|
|
|
17
16
|
/** The most related locations one result carries; the report stays complete beyond them. */
|
|
18
17
|
const MAX_RELATED = 100;
|
|
@@ -41,23 +40,26 @@ function firstSentence(text: string): string {
|
|
|
41
40
|
return text;
|
|
42
41
|
}
|
|
43
42
|
|
|
43
|
+
/** Every live kind whose languages include one of the run's, in code order. */
|
|
44
|
+
function kindsOf(languages: readonly string[]): readonly KindRow[] {
|
|
45
|
+
return [...KINDS.values()].filter((kind) =>
|
|
46
|
+
kind.languages.some((language) => languages.includes(language)),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
44
50
|
/**
|
|
45
|
-
* One rule per live kind of
|
|
51
|
+
* One rule per live kind of the run's languages, in code order, whether or not a result
|
|
46
52
|
* names it, so a rule's index is the same in every run and under every configuration.
|
|
47
53
|
*/
|
|
48
|
-
function rules(): readonly Record<string, unknown>[] {
|
|
49
|
-
return
|
|
50
|
-
const kind = KINDS.get(code);
|
|
51
|
-
if (kind === undefined) {
|
|
52
|
-
throw new RenderError(`the rule ${code} names no live kind`);
|
|
53
|
-
}
|
|
54
|
+
function rules(kinds: readonly KindRow[]): readonly Record<string, unknown>[] {
|
|
55
|
+
return kinds.map((kind) => {
|
|
54
56
|
return {
|
|
55
|
-
id: code,
|
|
57
|
+
id: kind.code,
|
|
56
58
|
name: kind.name,
|
|
57
|
-
shortDescription: { text: firstSentence(
|
|
58
|
-
fullDescription: { text:
|
|
59
|
+
shortDescription: { text: firstSentence(kind.rule) },
|
|
60
|
+
fullDescription: { text: kind.rule },
|
|
59
61
|
help: {
|
|
60
|
-
text:
|
|
62
|
+
text: kind.precondition === undefined ? kind.rule : `${kind.rule}\n\n${kind.precondition}`,
|
|
61
63
|
},
|
|
62
64
|
defaultConfiguration: { level: LEVEL[kind.defaultSeverity] },
|
|
63
65
|
properties: {
|
|
@@ -155,6 +157,11 @@ function relatedOf(found: WireFinding): readonly Related[] {
|
|
|
155
157
|
for (const at of found.details.write_positions ?? []) {
|
|
156
158
|
related.push({ label: "write", at, endLine: at.end_line });
|
|
157
159
|
}
|
|
160
|
+
for (const one of found.component.members ?? []) {
|
|
161
|
+
if (one.ref !== found.symbol.ref) {
|
|
162
|
+
related.push({ label: "member", at: one.position, endLine: one.position.end_line });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
158
165
|
return related.slice(0, MAX_RELATED);
|
|
159
166
|
}
|
|
160
167
|
|
|
@@ -254,8 +261,9 @@ function staleResult(
|
|
|
254
261
|
* rendering, because a line fingerprint computed from other bytes opens a second alert.
|
|
255
262
|
*/
|
|
256
263
|
export function sarif(report: Report, options: RenderOptions): string {
|
|
257
|
-
const
|
|
258
|
-
const
|
|
264
|
+
const kinds = kindsOf(report.analyzer.languages);
|
|
265
|
+
const ruleList = rules(kinds);
|
|
266
|
+
const index = new Map(kinds.map((kind, at) => [kind.code, at]));
|
|
259
267
|
const hashOf = lineHashCache(options.readSource);
|
|
260
268
|
const results = [
|
|
261
269
|
...report.findings.map((found) => findingResult(found, index, hashOf)),
|
|
@@ -115,7 +115,11 @@ const ESCAPES: Readonly<Record<string, string>> = {
|
|
|
115
115
|
const ESCAPE_WIDTHS: Readonly<Record<string, number>> = { x: 2, u: 4, U: 8 };
|
|
116
116
|
|
|
117
117
|
const IDENTIFIER = /[A-Za-z_][A-Za-z0-9_]*/uy;
|
|
118
|
-
|
|
118
|
+
/** A number as the lexer reads one, before the subset decides whether it is a decimal integer. */
|
|
119
|
+
const NUMBER = /[+-]?[0-9][0-9A-Za-z_.]*/uy;
|
|
120
|
+
|
|
121
|
+
/** The one number form the subset admits: a decimal integer with no leading zero. */
|
|
122
|
+
const DECIMAL_INTEGER = /^[+-]?(?:0|[1-9][0-9]*)$/u;
|
|
119
123
|
|
|
120
124
|
function lineOf(source: string, at: number): number {
|
|
121
125
|
let line = 1;
|
|
@@ -224,6 +228,11 @@ function lexAction(
|
|
|
224
228
|
NUMBER.lastIndex = i;
|
|
225
229
|
const number = NUMBER.exec(source)?.[0];
|
|
226
230
|
if (number !== undefined) {
|
|
231
|
+
if (!DECIMAL_INTEGER.test(number)) {
|
|
232
|
+
throw new TemplateError(
|
|
233
|
+
`line ${String(line)}: ${JSON.stringify(number)} is not a decimal integer`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
227
236
|
tokens.push({ t: "lit", value: Number(number) });
|
|
228
237
|
i += number.length;
|
|
229
238
|
continue;
|
|
@@ -636,7 +645,61 @@ function compareBasic(name: string, a: Value, b: Value): number {
|
|
|
636
645
|
if (kind === "boolean") {
|
|
637
646
|
return Number.NaN;
|
|
638
647
|
}
|
|
639
|
-
|
|
648
|
+
if (kind === "string") {
|
|
649
|
+
return byCodePoints(a as string, b as string);
|
|
650
|
+
}
|
|
651
|
+
return (a as number) < (b as number) ? -1 : 1;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Two strings ordered by the bytes of their UTF-8 encodings, which is the order of their
|
|
656
|
+
* code points: the order of their UTF-16 code units differs above U+FFFF.
|
|
657
|
+
*/
|
|
658
|
+
function byCodePoints(a: string, b: string): number {
|
|
659
|
+
let at = 0;
|
|
660
|
+
while (at < a.length && at < b.length) {
|
|
661
|
+
const x = a.codePointAt(at) ?? 0;
|
|
662
|
+
const y = b.codePointAt(at) ?? 0;
|
|
663
|
+
if (x !== y) {
|
|
664
|
+
return x < y ? -1 : 1;
|
|
665
|
+
}
|
|
666
|
+
at += x > 0xffff ? 2 : 1;
|
|
667
|
+
}
|
|
668
|
+
return a.length - b.length;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** The escapes `%q` writes for the control characters that have one of their own. */
|
|
672
|
+
const QUOTED_ESCAPES: Readonly<Record<string, string>> = {
|
|
673
|
+
'"': '\\"',
|
|
674
|
+
"\\": "\\\\",
|
|
675
|
+
"\u0007": "\\a",
|
|
676
|
+
"\b": "\\b",
|
|
677
|
+
"\f": "\\f",
|
|
678
|
+
"\n": "\\n",
|
|
679
|
+
"\r": "\\r",
|
|
680
|
+
"\t": "\\t",
|
|
681
|
+
"\v": "\\v",
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* The text `%q` writes: double quotes around the text, a quote and a backslash escaped,
|
|
686
|
+
* each control character with an escape of its own written as it, every other character
|
|
687
|
+
* below U+0020 and U+007F written as `\x` and two lowercase hexadecimal digits, and every
|
|
688
|
+
* other character as itself.
|
|
689
|
+
*/
|
|
690
|
+
function goQuoted(text: string): string {
|
|
691
|
+
let out = '"';
|
|
692
|
+
for (const char of text) {
|
|
693
|
+
const code = char.codePointAt(0) ?? 0;
|
|
694
|
+
if (Object.hasOwn(QUOTED_ESCAPES, char)) {
|
|
695
|
+
out += QUOTED_ESCAPES[char] ?? "";
|
|
696
|
+
} else if (code < 0x20 || code === 0x7f) {
|
|
697
|
+
out += `\\x${code.toString(16).padStart(2, "0")}`;
|
|
698
|
+
} else {
|
|
699
|
+
out += char;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return `${out}"`;
|
|
640
703
|
}
|
|
641
704
|
|
|
642
705
|
/** `printf`'s verbs, each over one operand. */
|
|
@@ -663,10 +726,10 @@ function printf(format: string, args: readonly Value[]): string {
|
|
|
663
726
|
switch (verb) {
|
|
664
727
|
case "v":
|
|
665
728
|
case "s":
|
|
666
|
-
out += formatted(arg,
|
|
729
|
+
out += formatted(arg, true);
|
|
667
730
|
break;
|
|
668
731
|
case "q":
|
|
669
|
-
out +=
|
|
732
|
+
out += goQuoted(formatted(arg, true));
|
|
670
733
|
break;
|
|
671
734
|
case "d":
|
|
672
735
|
if (typeof arg !== "number" || !Number.isInteger(arg)) {
|
|
@@ -768,11 +831,11 @@ function call(name: string, args: readonly Value[]): Value {
|
|
|
768
831
|
return args
|
|
769
832
|
.map((arg, i) => {
|
|
770
833
|
const spaced = i > 0 && typeof arg !== "string" && typeof args[i - 1] !== "string";
|
|
771
|
-
return `${spaced ? " " : ""}${formatted(arg,
|
|
834
|
+
return `${spaced ? " " : ""}${formatted(arg, true)}`;
|
|
772
835
|
})
|
|
773
836
|
.join("");
|
|
774
837
|
case "println":
|
|
775
|
-
return `${args.map((arg) => formatted(arg,
|
|
838
|
+
return `${args.map((arg) => formatted(arg, true)).join(" ")}\n`;
|
|
776
839
|
case "printf":
|
|
777
840
|
if (typeof first !== "string") {
|
|
778
841
|
throw new RenderError("printf takes a format string first");
|
|
@@ -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 {
|
|
@@ -57,7 +58,7 @@ import type { DetectorInput, Evidence } from "./exempt.ts";
|
|
|
57
58
|
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
58
59
|
import { renderPosition } from "./position.ts";
|
|
59
60
|
import type { ProjectView } from "./session.ts";
|
|
60
|
-
import { valueReach } from "./value-reach.ts";
|
|
61
|
+
import { valueReach, type Reached } from "./value-reach.ts";
|
|
61
62
|
|
|
62
63
|
/** The serializer every run reads, whatever the configuration names. */
|
|
63
64
|
const JSON_STRINGIFY: DeclarationEntry = { shape: "global", global: "JSON.stringify" };
|
|
@@ -65,6 +66,9 @@ const JSON_STRINGIFY: DeclarationEntry = { shape: "global", global: "JSON.string
|
|
|
65
66
|
/** The methods a serializer and a formatter resolve by name on a value that left the analysis. */
|
|
66
67
|
const CONVERSIONS: ReadonlySet<string> = new Set(["toJSON", "toString"]);
|
|
67
68
|
|
|
69
|
+
/** The method a serializer calls on a value that carries one, wherever the value goes. */
|
|
70
|
+
const SERIALIZED_CONVERSION = "toJSON";
|
|
71
|
+
|
|
68
72
|
/** The parameter types that keep nothing of the value they are given. */
|
|
69
73
|
const ERASED = TypeFlags.Any | TypeFlags.Unknown;
|
|
70
74
|
|
|
@@ -521,10 +525,12 @@ function destinations<Brand>(
|
|
|
521
525
|
return { invocation, at, mayReceive };
|
|
522
526
|
}
|
|
523
527
|
|
|
524
|
-
/** One argument whose value carries classes of the target into a destination. */
|
|
528
|
+
/** One argument whose value carries classes and object types of the target into a destination. */
|
|
525
529
|
interface Flow {
|
|
526
530
|
readonly site: Node;
|
|
527
531
|
readonly classes: ReadonlySet<string>;
|
|
532
|
+
/** The members of the interfaces and object types the value carries. */
|
|
533
|
+
readonly members: readonly TSSymbol[];
|
|
528
534
|
readonly to: Destination;
|
|
529
535
|
}
|
|
530
536
|
|
|
@@ -554,31 +560,34 @@ function flowsOf<Brand>(
|
|
|
554
560
|
);
|
|
555
561
|
return arguments_.flatMap((one) => {
|
|
556
562
|
const type = typeOf.get(one.value);
|
|
557
|
-
let
|
|
563
|
+
let reached: Reached = { classes: new Set<string>(), members: [] };
|
|
558
564
|
if (one.value.kind === SyntaxKind.ThisKeyword) {
|
|
559
|
-
|
|
565
|
+
reached = {
|
|
566
|
+
classes: new Set(one.site.self === undefined ? [] : [one.site.self]),
|
|
567
|
+
members: [],
|
|
568
|
+
};
|
|
560
569
|
} else if (type !== undefined) {
|
|
561
|
-
|
|
570
|
+
reached = reach.reachedFrom(type);
|
|
562
571
|
}
|
|
563
|
-
const
|
|
564
|
-
|
|
572
|
+
const carries = reached.classes.size > 0 || reached.members.length > 0;
|
|
573
|
+
const destination = carries ? to.at(one.site.call, one.argument) : undefined;
|
|
574
|
+
return destination === undefined
|
|
575
|
+
? []
|
|
576
|
+
: [{ site: one.value, classes: reached.classes, members: reached.members, to: destination }];
|
|
565
577
|
});
|
|
566
578
|
}
|
|
567
579
|
|
|
568
580
|
/**
|
|
569
|
-
* The serialization-contract detector: each
|
|
570
|
-
*
|
|
571
|
-
* analysis, recorded at the argument with
|
|
581
|
+
* The serialization-contract detector: each data member and `toJSON` of each class,
|
|
582
|
+
* interface and object type a value that reaches a destination carries, and `toString`
|
|
583
|
+
* as well where the destination is outside the analysis, recorded at the argument with
|
|
584
|
+
* the callee named.
|
|
572
585
|
*/
|
|
573
586
|
export function serializationContract<Brand>(input: DetectorInput<Brand>): readonly Evidence[] {
|
|
574
587
|
const { project, held, targetRoot } = input;
|
|
575
588
|
const walked = walkProject(project, held);
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
held,
|
|
579
|
-
walked.sites,
|
|
580
|
-
destinations(input, aliasChains(project, held), walked),
|
|
581
|
-
);
|
|
589
|
+
const chains = aliasChains(project, held);
|
|
590
|
+
const flows = flowsOf(project, held, walked.sites, destinations(input, chains, walked));
|
|
582
591
|
if (flows.length === 0) {
|
|
583
592
|
return [];
|
|
584
593
|
}
|
|
@@ -589,22 +598,30 @@ export function serializationContract<Brand>(input: DetectorInput<Brand>): reado
|
|
|
589
598
|
"instance",
|
|
590
599
|
);
|
|
591
600
|
const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
|
|
601
|
+
// A destination holds back a data member and `toJSON`, and `toString` as well where
|
|
602
|
+
// the value left the analysis.
|
|
603
|
+
const holdsBack = (flags: SymbolFlags, name: string, to: Destination): boolean =>
|
|
604
|
+
(flags & SymbolFlags.Property) !== 0 ||
|
|
605
|
+
((flags & SymbolFlags.Method) !== 0 &&
|
|
606
|
+
(name === SERIALIZED_CONVERSION || (to.reach === "conversions" && CONVERSIONS.has(name))));
|
|
592
607
|
const found: Evidence[] = [];
|
|
593
608
|
for (const flow of flows) {
|
|
594
609
|
const site = renderPosition(flow.site.getSourceFile(), targetRoot, flow.site.getStart());
|
|
595
610
|
const detail = `passed to ${flow.to.callee}`;
|
|
596
611
|
for (const member of [...flow.classes].flatMap((id) => members.get(id) ?? [])) {
|
|
597
612
|
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) {
|
|
613
|
+
const name = symbol === undefined ? "" : (memberComponent(symbol, byId) ?? "");
|
|
614
|
+
if (holdsBack(member.flags, name, flow.to)) {
|
|
605
615
|
found.push({ id: member.id, detail, site });
|
|
606
616
|
}
|
|
607
617
|
}
|
|
618
|
+
for (const member of flow.members) {
|
|
619
|
+
if (holdsBack(member.flags, member.name, flow.to)) {
|
|
620
|
+
for (const id of chains.declarationsOf(member)) {
|
|
621
|
+
found.push({ id, detail, site });
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
608
625
|
}
|
|
609
626
|
return found;
|
|
610
627
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The function a declaration of the inventory declares, where it declares one: the node
|
|
3
|
+
* whose parameters, body and result are the declaration's own parts.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
isArrowFunction,
|
|
8
|
+
isClassDeclaration,
|
|
9
|
+
isClassExpression,
|
|
10
|
+
isFunctionDeclaration,
|
|
11
|
+
isFunctionExpression,
|
|
12
|
+
isMethodDeclaration,
|
|
13
|
+
isPropertyDeclaration,
|
|
14
|
+
isVariableDeclaration,
|
|
15
|
+
type ArrowFunction,
|
|
16
|
+
type FunctionDeclaration,
|
|
17
|
+
type FunctionExpression,
|
|
18
|
+
type MethodDeclaration,
|
|
19
|
+
type Node,
|
|
20
|
+
} from "@typescript/native/unstable/ast";
|
|
21
|
+
|
|
22
|
+
/** A function-like node that carries a body of its own. */
|
|
23
|
+
export type SignatureNode =
|
|
24
|
+
FunctionDeclaration | MethodDeclaration | ArrowFunction | FunctionExpression;
|
|
25
|
+
|
|
26
|
+
/** Whether one node is a function value written in place: an arrow or a function expression. */
|
|
27
|
+
function isFunctionValue(node: Node | undefined): node is ArrowFunction | FunctionExpression {
|
|
28
|
+
return node !== undefined && (isArrowFunction(node) || isFunctionExpression(node));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The function one declaration declares: a function declaration and a class method with
|
|
33
|
+
* a body are their own; a variable and a class property whose initializer is an arrow or
|
|
34
|
+
* a function expression declare that function. Any other declaration declares none, an
|
|
35
|
+
* overload signature and an abstract or ambient method among them, because no body is
|
|
36
|
+
* there to read its parts.
|
|
37
|
+
*/
|
|
38
|
+
export function signatureOf(node: Node): SignatureNode | undefined {
|
|
39
|
+
if (isFunctionDeclaration(node)) {
|
|
40
|
+
return node.body === undefined ? undefined : node;
|
|
41
|
+
}
|
|
42
|
+
if (isMethodDeclaration(node)) {
|
|
43
|
+
const owner = node.parent;
|
|
44
|
+
return node.body !== undefined && (isClassDeclaration(owner) || isClassExpression(owner))
|
|
45
|
+
? node
|
|
46
|
+
: undefined;
|
|
47
|
+
}
|
|
48
|
+
if (
|
|
49
|
+
(isVariableDeclaration(node) || isPropertyDeclaration(node)) &&
|
|
50
|
+
isFunctionValue(node.initializer)
|
|
51
|
+
) {
|
|
52
|
+
return node.initializer;
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
package/src/suppress-file.ts
CHANGED
|
@@ -154,6 +154,15 @@ function readRecord(value: unknown, site: Position, byReference: ByReference, sh
|
|
|
154
154
|
return { records: named.map((one) => ({ ...written, bound: one.id })), refusals: [] };
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
function byReferenceOf(symbols: readonly InventorySymbol[]): ByReference {
|
|
158
|
+
const byReference = new Map<string, InventorySymbol[]>();
|
|
159
|
+
for (const symbol of symbols) {
|
|
160
|
+
const key = referenceKey(symbol.ref, symbol.position.path);
|
|
161
|
+
byReference.set(key, [...(byReference.get(key) ?? []), symbol]);
|
|
162
|
+
}
|
|
163
|
+
return byReference;
|
|
164
|
+
}
|
|
165
|
+
|
|
157
166
|
/** Reads one document at the target root, binding each record against the run's declarations. */
|
|
158
167
|
function readDocument(
|
|
159
168
|
host: Host,
|
|
@@ -196,11 +205,7 @@ function readDocument(
|
|
|
196
205
|
return malformed(shape.file);
|
|
197
206
|
}
|
|
198
207
|
|
|
199
|
-
const byReference =
|
|
200
|
-
for (const symbol of symbols) {
|
|
201
|
-
const key = referenceKey(symbol.ref, symbol.position.path);
|
|
202
|
-
byReference.set(key, [...(byReference.get(key) ?? []), symbol]);
|
|
203
|
-
}
|
|
208
|
+
const byReference = byReferenceOf(symbols);
|
|
204
209
|
const records: SuppressionRecord[] = [];
|
|
205
210
|
const refusals: Refusal[] = [];
|
|
206
211
|
(held as readonly unknown[]).forEach((value, index) => {
|
|
@@ -247,6 +252,24 @@ export interface Recorded {
|
|
|
247
252
|
readonly path: string;
|
|
248
253
|
}
|
|
249
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Rows a baseline write holds in memory, bound against the run's declarations as the same
|
|
257
|
+
* rows read back from the document would be, each carrying the reason given.
|
|
258
|
+
*/
|
|
259
|
+
export function bindRows(
|
|
260
|
+
rows: readonly Recorded[],
|
|
261
|
+
reason: string,
|
|
262
|
+
symbols: readonly InventorySymbol[],
|
|
263
|
+
): Suppressions {
|
|
264
|
+
const byReference = byReferenceOf(symbols);
|
|
265
|
+
const site = documentSite(BASELINE_SHAPE);
|
|
266
|
+
const read = rows.map((row) => readRecord({ ...row, reason }, site, byReference, BASELINE_SHAPE));
|
|
267
|
+
return {
|
|
268
|
+
records: read.flatMap((one) => one.records),
|
|
269
|
+
refusals: read.flatMap((one) => one.refusals),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
250
273
|
/** The analyzer a written row names as its provenance, as the same run's report names it. */
|
|
251
274
|
export interface Provenance {
|
|
252
275
|
readonly analyzer: string;
|
|
@@ -258,6 +281,11 @@ const BASELINE_DESCRIPTION =
|
|
|
258
281
|
"Recorded findings: a run fails only on a finding this document does not hold. " +
|
|
259
282
|
"Every reason is the analyzer that recorded the row, never an adjudication.";
|
|
260
283
|
|
|
284
|
+
/** The reason every row a baseline write records carries. */
|
|
285
|
+
export function rowReason(provenance: Provenance): string {
|
|
286
|
+
return `recorded by ${provenance.analyzer} ${provenance.version}`;
|
|
287
|
+
}
|
|
288
|
+
|
|
261
289
|
/**
|
|
262
290
|
* A baseline holding one row per finding, in the order given, which is the report's. Every
|
|
263
291
|
* row carries the provenance as its reason. A finding missing a value a row needs, and an
|
|
@@ -270,7 +298,7 @@ export function writeBaseline(findings: readonly Recorded[], provenance: Provena
|
|
|
270
298
|
`a baseline row would carry no reason: the analyzer identity names ${JSON.stringify(provenance.analyzer)} at version ${JSON.stringify(provenance.version)}`,
|
|
271
299
|
);
|
|
272
300
|
}
|
|
273
|
-
const reason =
|
|
301
|
+
const reason = rowReason(provenance);
|
|
274
302
|
const baseline = findings.map((found) => {
|
|
275
303
|
if (found.code === "" || found.symbol === "" || found.path === "") {
|
|
276
304
|
throw new Error(
|
package/src/suppress-inline.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { SyntaxKind, type Node, type SourceFile } from "@typescript/native/unsta
|
|
|
8
8
|
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
9
9
|
import { byPosition, positionKey, renderPosition, type Position } from "./position.ts";
|
|
10
10
|
import type { ProjectView } from "./session.ts";
|
|
11
|
+
import { signatureOf } from "./signatures.ts";
|
|
11
12
|
import {
|
|
12
13
|
SUPPRESSION_WITHOUT_REASON,
|
|
13
14
|
SuppressionError,
|
|
@@ -168,7 +169,8 @@ interface Declared {
|
|
|
168
169
|
/**
|
|
169
170
|
* The declarations of one file by the line each begins on, the line of its first token: a
|
|
170
171
|
* decorator, a modifier or the keyword. A variable's first token is its statement's
|
|
171
|
-
* keyword, and a variable written on a later line of the statement also begins there.
|
|
172
|
+
* keyword, and a variable written on a later line of the statement also begins there. A
|
|
173
|
+
* function also begins, for a directive, on the line of each of its parameters.
|
|
172
174
|
*/
|
|
173
175
|
function declarationLines(
|
|
174
176
|
file: SourceFile,
|
|
@@ -189,6 +191,11 @@ function declarationLines(
|
|
|
189
191
|
const one = id === undefined ? undefined : symbols.get(id);
|
|
190
192
|
if (one !== undefined) {
|
|
191
193
|
add(node.pos, one);
|
|
194
|
+
// A parameter on a later line of a wrapped signature takes its directive on the
|
|
195
|
+
// line above it, inside the parameter list, and the directive binds the function.
|
|
196
|
+
for (const parameter of signatureOf(node)?.parameters ?? []) {
|
|
197
|
+
add(parameter.pos, one);
|
|
198
|
+
}
|
|
192
199
|
const list = node.parent;
|
|
193
200
|
if (
|
|
194
201
|
node.kind === SyntaxKind.VariableDeclaration &&
|
package/src/suppress.ts
CHANGED
|
@@ -169,6 +169,7 @@ const ROW_SUBJECTS: ReadonlySet<string> = new Set([
|
|
|
169
169
|
"dependency",
|
|
170
170
|
"module-directive",
|
|
171
171
|
"root",
|
|
172
|
+
"configured-declaration",
|
|
172
173
|
"suppression",
|
|
173
174
|
"edge",
|
|
174
175
|
]);
|
|
@@ -178,6 +179,36 @@ export function isRowSubject(kind: string): boolean {
|
|
|
178
179
|
return ROW_SUBJECTS.has(kind);
|
|
179
180
|
}
|
|
180
181
|
|
|
182
|
+
/**
|
|
183
|
+
* The subject kinds that are a part of a declaration rather than a declaration: a part
|
|
184
|
+
* has no reference of its own, so a finding about one names the declaration that holds
|
|
185
|
+
* it, and the part is decided inside that declaration whatever uses it.
|
|
186
|
+
*/
|
|
187
|
+
const PART_SUBJECTS: ReadonlySet<string> = new Set([
|
|
188
|
+
"parameter",
|
|
189
|
+
"receiver",
|
|
190
|
+
"result",
|
|
191
|
+
"statement",
|
|
192
|
+
"case",
|
|
193
|
+
"store",
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
/** Whether a finding's subject is a part of the declaration its reference names. */
|
|
197
|
+
export function isPartSubject(kind: string): boolean {
|
|
198
|
+
return PART_SUBJECTS.has(kind);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The prefix of the codes of the part kinds, the intra-function range. */
|
|
202
|
+
const PART_KIND_PREFIX = "DS18";
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Whether a code is a part kind, whose record withholds its finding and marks nothing:
|
|
206
|
+
* the record says the part is wanted, not that the declaration holding it is.
|
|
207
|
+
*/
|
|
208
|
+
export function isPartKind(code: string): boolean {
|
|
209
|
+
return code.startsWith(PART_KIND_PREFIX);
|
|
210
|
+
}
|
|
211
|
+
|
|
181
212
|
/** One finding's identity: its code and the position of the thing it names. */
|
|
182
213
|
function claimKey(finding: Finding): string {
|
|
183
214
|
return `${finding.code}\u0000${positionKey(finding.position)}`;
|
package/src/value-reach.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The classes of the target a value of one type carries,
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* The classes of the target a value of one type carries, and the members of the other
|
|
3
|
+
* object types the target writes that it carries, as a serializer walking the value
|
|
4
|
+
* reaches them: through union and intersection constituents, array and tuple elements,
|
|
5
|
+
* and the data members and index signatures of the target's classes and of the
|
|
6
|
+
* interfaces and object types the target writes. The walk stops at `any`, `unknown`, a
|
|
7
|
+
* type parameter, a function and an interface or class of another program, so an
|
|
8
|
+
* instance held only there is missed.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
import {
|
|
@@ -23,12 +25,25 @@ const WRITTEN = ObjectFlags.ObjectLiteral | ObjectFlags.Mapped;
|
|
|
23
25
|
/** The symbols of an anonymous object type the target writes as a type. */
|
|
24
26
|
const LITERAL_SYMBOLS = SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral;
|
|
25
27
|
|
|
26
|
-
/**
|
|
28
|
+
/** What a value of one type carries that the target declares. */
|
|
29
|
+
export interface Reached {
|
|
30
|
+
/** The identifiers of the target's classes. */
|
|
31
|
+
readonly classes: ReadonlySet<string>;
|
|
32
|
+
/**
|
|
33
|
+
* The members of the interfaces and object types the target writes, other than its
|
|
34
|
+
* classes: each data member, and each method named `toJSON` or `toString`.
|
|
35
|
+
*/
|
|
36
|
+
readonly members: readonly TSSymbol[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** What each type's values carry, each type asked of the checker once. */
|
|
27
40
|
export interface ValueReach {
|
|
28
|
-
|
|
29
|
-
classesOf(type: Type): ReadonlySet<string>;
|
|
41
|
+
reachedFrom(type: Type): Reached;
|
|
30
42
|
}
|
|
31
43
|
|
|
44
|
+
/** The two conversion methods a serializer or a formatter resolves on a value's own shape. */
|
|
45
|
+
const CONVERSION_NAMES: ReadonlySet<string> = new Set(["toJSON", "toString"]);
|
|
46
|
+
|
|
32
47
|
/** The global array types, whose element type is what an array value carries. */
|
|
33
48
|
function arrayTargets<Brand>(project: ProjectView<Brand>): ReadonlySet<number> {
|
|
34
49
|
const checker = project.checker;
|
|
@@ -105,6 +120,29 @@ export function valueReach<Brand>(project: ProjectView<Brand>, held: Inventory):
|
|
|
105
120
|
];
|
|
106
121
|
};
|
|
107
122
|
|
|
123
|
+
/** Whether the walk reads one type's members because the target writes the type, not as a class. */
|
|
124
|
+
const writtenObject = (type: Type): boolean => {
|
|
125
|
+
if (!type.isObjectType() || classOf(type) !== undefined) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
const target = targetOf(type);
|
|
129
|
+
const isList =
|
|
130
|
+
type.isTypeReference() &&
|
|
131
|
+
(arrays.has(target.id) ||
|
|
132
|
+
(target.isObjectType() && (target.objectFlags & ObjectFlags.Tuple) !== 0));
|
|
133
|
+
return !isList && isWritten(type);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/** The data members and conversion methods of one written object type. */
|
|
137
|
+
const carried = (type: Type): readonly TSSymbol[] =>
|
|
138
|
+
checker
|
|
139
|
+
.getPropertiesOfType(type)
|
|
140
|
+
.filter(
|
|
141
|
+
(property) =>
|
|
142
|
+
(property.flags & SymbolFlags.Property) !== 0 ||
|
|
143
|
+
((property.flags & SymbolFlags.Method) !== 0 && CONVERSION_NAMES.has(property.name)),
|
|
144
|
+
);
|
|
145
|
+
|
|
108
146
|
/** The types a value of one type holds, read once per type. */
|
|
109
147
|
const childrenOf = (type: Type): readonly Type[] => {
|
|
110
148
|
const known = children.get(type.id);
|
|
@@ -131,8 +169,9 @@ export function valueReach<Brand>(project: ProjectView<Brand>, held: Inventory):
|
|
|
131
169
|
};
|
|
132
170
|
|
|
133
171
|
return {
|
|
134
|
-
|
|
135
|
-
const
|
|
172
|
+
reachedFrom: (type) => {
|
|
173
|
+
const classes = new Set<string>();
|
|
174
|
+
const members: TSSymbol[] = [];
|
|
136
175
|
const seen = new Set<number>();
|
|
137
176
|
const pending: Type[] = [type];
|
|
138
177
|
for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
|
|
@@ -142,11 +181,13 @@ export function valueReach<Brand>(project: ProjectView<Brand>, held: Inventory):
|
|
|
142
181
|
seen.add(next.id);
|
|
143
182
|
const owner = classOf(next);
|
|
144
183
|
if (owner !== undefined) {
|
|
145
|
-
|
|
184
|
+
classes.add(owner);
|
|
185
|
+
} else if (writtenObject(next)) {
|
|
186
|
+
members.push(...carried(next));
|
|
146
187
|
}
|
|
147
188
|
pending.push(...childrenOf(next));
|
|
148
189
|
}
|
|
149
|
-
return
|
|
190
|
+
return { classes, members };
|
|
150
191
|
},
|
|
151
192
|
};
|
|
152
193
|
}
|