@cplieger/deadset-ts 3.12.0 → 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/graph.js +7 -7
- 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/roots.js +13 -8
- 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/graph.ts +7 -7
- 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/roots.ts +14 -9
- 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
|
@@ -49,7 +49,10 @@ const ESCAPES = {
|
|
|
49
49
|
/** The number of hexadecimal digits each numeric escape takes. */
|
|
50
50
|
const ESCAPE_WIDTHS = { x: 2, u: 4, U: 8 };
|
|
51
51
|
const IDENTIFIER = /[A-Za-z_][A-Za-z0-9_]*/uy;
|
|
52
|
-
|
|
52
|
+
/** A number as the lexer reads one, before the subset decides whether it is a decimal integer. */
|
|
53
|
+
const NUMBER = /[+-]?[0-9][0-9A-Za-z_.]*/uy;
|
|
54
|
+
/** The one number form the subset admits: a decimal integer with no leading zero. */
|
|
55
|
+
const DECIMAL_INTEGER = /^[+-]?(?:0|[1-9][0-9]*)$/u;
|
|
53
56
|
function lineOf(source, at) {
|
|
54
57
|
let line = 1;
|
|
55
58
|
for (let i = 0; i < at; i += 1) {
|
|
@@ -153,6 +156,9 @@ function lexAction(source, at, line) {
|
|
|
153
156
|
NUMBER.lastIndex = i;
|
|
154
157
|
const number = NUMBER.exec(source)?.[0];
|
|
155
158
|
if (number !== undefined) {
|
|
159
|
+
if (!DECIMAL_INTEGER.test(number)) {
|
|
160
|
+
throw new TemplateError(`line ${String(line)}: ${JSON.stringify(number)} is not a decimal integer`);
|
|
161
|
+
}
|
|
156
162
|
tokens.push({ t: "lit", value: Number(number) });
|
|
157
163
|
i += number.length;
|
|
158
164
|
continue;
|
|
@@ -530,8 +536,61 @@ function compareBasic(name, a, b) {
|
|
|
530
536
|
if (kind === "boolean") {
|
|
531
537
|
return Number.NaN;
|
|
532
538
|
}
|
|
539
|
+
if (kind === "string") {
|
|
540
|
+
return byCodePoints(a, b);
|
|
541
|
+
}
|
|
533
542
|
return a < b ? -1 : 1;
|
|
534
543
|
}
|
|
544
|
+
/**
|
|
545
|
+
* Two strings ordered by the bytes of their UTF-8 encodings, which is the order of their
|
|
546
|
+
* code points: the order of their UTF-16 code units differs above U+FFFF.
|
|
547
|
+
*/
|
|
548
|
+
function byCodePoints(a, b) {
|
|
549
|
+
let at = 0;
|
|
550
|
+
while (at < a.length && at < b.length) {
|
|
551
|
+
const x = a.codePointAt(at) ?? 0;
|
|
552
|
+
const y = b.codePointAt(at) ?? 0;
|
|
553
|
+
if (x !== y) {
|
|
554
|
+
return x < y ? -1 : 1;
|
|
555
|
+
}
|
|
556
|
+
at += x > 0xffff ? 2 : 1;
|
|
557
|
+
}
|
|
558
|
+
return a.length - b.length;
|
|
559
|
+
}
|
|
560
|
+
/** The escapes `%q` writes for the control characters that have one of their own. */
|
|
561
|
+
const QUOTED_ESCAPES = {
|
|
562
|
+
'"': '\\"',
|
|
563
|
+
"\\": "\\\\",
|
|
564
|
+
"\u0007": "\\a",
|
|
565
|
+
"\b": "\\b",
|
|
566
|
+
"\f": "\\f",
|
|
567
|
+
"\n": "\\n",
|
|
568
|
+
"\r": "\\r",
|
|
569
|
+
"\t": "\\t",
|
|
570
|
+
"\v": "\\v",
|
|
571
|
+
};
|
|
572
|
+
/**
|
|
573
|
+
* The text `%q` writes: double quotes around the text, a quote and a backslash escaped,
|
|
574
|
+
* each control character with an escape of its own written as it, every other character
|
|
575
|
+
* below U+0020 and U+007F written as `\x` and two lowercase hexadecimal digits, and every
|
|
576
|
+
* other character as itself.
|
|
577
|
+
*/
|
|
578
|
+
function goQuoted(text) {
|
|
579
|
+
let out = '"';
|
|
580
|
+
for (const char of text) {
|
|
581
|
+
const code = char.codePointAt(0) ?? 0;
|
|
582
|
+
if (Object.hasOwn(QUOTED_ESCAPES, char)) {
|
|
583
|
+
out += QUOTED_ESCAPES[char] ?? "";
|
|
584
|
+
}
|
|
585
|
+
else if (code < 0x20 || code === 0x7f) {
|
|
586
|
+
out += `\\x${code.toString(16).padStart(2, "0")}`;
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
out += char;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return `${out}"`;
|
|
593
|
+
}
|
|
535
594
|
/** `printf`'s verbs, each over one operand. */
|
|
536
595
|
function printf(format, args) {
|
|
537
596
|
let out = "";
|
|
@@ -556,10 +615,10 @@ function printf(format, args) {
|
|
|
556
615
|
switch (verb) {
|
|
557
616
|
case "v":
|
|
558
617
|
case "s":
|
|
559
|
-
out += formatted(arg,
|
|
618
|
+
out += formatted(arg, true);
|
|
560
619
|
break;
|
|
561
620
|
case "q":
|
|
562
|
-
out +=
|
|
621
|
+
out += goQuoted(formatted(arg, true));
|
|
563
622
|
break;
|
|
564
623
|
case "d":
|
|
565
624
|
if (typeof arg !== "number" || !Number.isInteger(arg)) {
|
|
@@ -659,11 +718,11 @@ function call(name, args) {
|
|
|
659
718
|
return args
|
|
660
719
|
.map((arg, i) => {
|
|
661
720
|
const spaced = i > 0 && typeof arg !== "string" && typeof args[i - 1] !== "string";
|
|
662
|
-
return `${spaced ? " " : ""}${formatted(arg,
|
|
721
|
+
return `${spaced ? " " : ""}${formatted(arg, true)}`;
|
|
663
722
|
})
|
|
664
723
|
.join("");
|
|
665
724
|
case "println":
|
|
666
|
-
return `${args.map((arg) => formatted(arg,
|
|
725
|
+
return `${args.map((arg) => formatted(arg, true)).join(" ")}\n`;
|
|
667
726
|
case "printf":
|
|
668
727
|
if (typeof first !== "string") {
|
|
669
728
|
throw new RenderError("printf takes a format string first");
|
package/dist/src/roots.js
CHANGED
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Every root seeds REACHABILITY, and no root counts as a reference. A root of a kind
|
|
6
6
|
* that names a caller is live under reference counting as well, which the sweep
|
|
7
|
-
* decides;
|
|
8
|
-
*
|
|
9
|
-
* candidate under reference counting.
|
|
7
|
+
* decides; the published API only supposes a consumer, so a published export is live
|
|
8
|
+
* under reachability and still a candidate under reference counting.
|
|
10
9
|
*/
|
|
11
10
|
import { aliasChains } from "./alias-chain.js";
|
|
12
11
|
import { entryPoints } from "./entry-points.js";
|
|
@@ -94,6 +93,7 @@ function childrenOf(held) {
|
|
|
94
93
|
* is a root with everything its module exports: each name of the module's export
|
|
95
94
|
* table, and where a name is a re-export, every link of the chain to the declaration
|
|
96
95
|
* it carries, so a re-exported declaration is a root of the file that publishes it.
|
|
96
|
+
* In a library, what a published file exports is rooted by the published API alone.
|
|
97
97
|
* A file a runtime executes without reading its exports, a test file or a worker, is
|
|
98
98
|
* a root by itself.
|
|
99
99
|
*/
|
|
@@ -116,14 +116,19 @@ export function roots(project, held, targetRoot, options) {
|
|
|
116
116
|
entered.push({ file, kind, source, exports });
|
|
117
117
|
};
|
|
118
118
|
const published = [];
|
|
119
|
-
const publishes = (entry) =>
|
|
119
|
+
const publishes = (entry) => options.publishedAPI &&
|
|
120
|
+
entry.published &&
|
|
120
121
|
(options.manifest.declaresExports ? entry.member.startsWith("exports") : true);
|
|
122
|
+
for (const entry of options.manifest.entries) {
|
|
123
|
+
if (publishes(entry)) {
|
|
124
|
+
published.push(...files.named(entry.path));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const publishedNames = new Set(published.map((file) => file.fileName));
|
|
121
128
|
for (const entry of options.manifest.entries) {
|
|
122
129
|
for (const file of files.named(entry.path)) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
published.push(file);
|
|
126
|
-
}
|
|
130
|
+
const kind = entry.role === "run" ? "manifest-binary" : "manifest-entry";
|
|
131
|
+
enter(file, kind, entry.member, !publishedNames.has(file.fileName));
|
|
127
132
|
}
|
|
128
133
|
}
|
|
129
134
|
for (const pattern of options.entryFiles) {
|
|
@@ -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";
|
|
@@ -20,6 +21,8 @@ import { valueReach } from "./value-reach.js";
|
|
|
20
21
|
const JSON_STRINGIFY = { shape: "global", global: "JSON.stringify" };
|
|
21
22
|
/** The methods a serializer and a formatter resolve by name on a value that left the analysis. */
|
|
22
23
|
const CONVERSIONS = new Set(["toJSON", "toString"]);
|
|
24
|
+
/** The method a serializer calls on a value that carries one, wherever the value goes. */
|
|
25
|
+
const SERIALIZED_CONVERSION = "toJSON";
|
|
23
26
|
/** The parameter types that keep nothing of the value they are given. */
|
|
24
27
|
const ERASED = TypeFlags.Any | TypeFlags.Unknown;
|
|
25
28
|
/** The wider of two reaches, where either may be absent. */
|
|
@@ -360,46 +363,62 @@ function flowsOf(project, held, sites, to) {
|
|
|
360
363
|
const typeOf = new Map(asked.map((one, index) => [one.value, types[index]]));
|
|
361
364
|
return arguments_.flatMap((one) => {
|
|
362
365
|
const type = typeOf.get(one.value);
|
|
363
|
-
let
|
|
366
|
+
let reached = { classes: new Set(), members: [] };
|
|
364
367
|
if (one.value.kind === SyntaxKind.ThisKeyword) {
|
|
365
|
-
|
|
368
|
+
reached = {
|
|
369
|
+
classes: new Set(one.site.self === undefined ? [] : [one.site.self]),
|
|
370
|
+
members: [],
|
|
371
|
+
};
|
|
366
372
|
}
|
|
367
373
|
else if (type !== undefined) {
|
|
368
|
-
|
|
374
|
+
reached = reach.reachedFrom(type);
|
|
369
375
|
}
|
|
370
|
-
const
|
|
371
|
-
|
|
376
|
+
const carries = reached.classes.size > 0 || reached.members.length > 0;
|
|
377
|
+
const destination = carries ? to.at(one.site.call, one.argument) : undefined;
|
|
378
|
+
return destination === undefined
|
|
379
|
+
? []
|
|
380
|
+
: [{ site: one.value, classes: reached.classes, members: reached.members, to: destination }];
|
|
372
381
|
});
|
|
373
382
|
}
|
|
374
383
|
/**
|
|
375
|
-
* The serialization-contract detector: each
|
|
376
|
-
*
|
|
377
|
-
* analysis, recorded at the argument with
|
|
384
|
+
* The serialization-contract detector: each data member and `toJSON` of each class,
|
|
385
|
+
* interface and object type a value that reaches a destination carries, and `toString`
|
|
386
|
+
* as well where the destination is outside the analysis, recorded at the argument with
|
|
387
|
+
* the callee named.
|
|
378
388
|
*/
|
|
379
389
|
export function serializationContract(input) {
|
|
380
390
|
const { project, held, targetRoot } = input;
|
|
381
391
|
const walked = walkProject(project, held);
|
|
382
|
-
const
|
|
392
|
+
const chains = aliasChains(project, held);
|
|
393
|
+
const flows = flowsOf(project, held, walked.sites, destinations(input, chains, walked));
|
|
383
394
|
if (flows.length === 0) {
|
|
384
395
|
return [];
|
|
385
396
|
}
|
|
386
397
|
const members = classMembers(project, held, new Set(flows.flatMap((flow) => [...flow.classes])), "instance");
|
|
387
398
|
const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
|
|
399
|
+
// A destination holds back a data member and `toJSON`, and `toString` as well where
|
|
400
|
+
// the value left the analysis.
|
|
401
|
+
const holdsBack = (flags, name, to) => (flags & SymbolFlags.Property) !== 0 ||
|
|
402
|
+
((flags & SymbolFlags.Method) !== 0 &&
|
|
403
|
+
(name === SERIALIZED_CONVERSION || (to.reach === "conversions" && CONVERSIONS.has(name))));
|
|
388
404
|
const found = [];
|
|
389
405
|
for (const flow of flows) {
|
|
390
406
|
const site = renderPosition(flow.site.getSourceFile(), targetRoot, flow.site.getStart());
|
|
391
407
|
const detail = `passed to ${flow.to.callee}`;
|
|
392
408
|
for (const member of [...flow.classes].flatMap((id) => members.get(id) ?? [])) {
|
|
393
409
|
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) {
|
|
410
|
+
const name = symbol === undefined ? "" : (memberComponent(symbol, byId) ?? "");
|
|
411
|
+
if (holdsBack(member.flags, name, flow.to)) {
|
|
400
412
|
found.push({ id: member.id, detail, site });
|
|
401
413
|
}
|
|
402
414
|
}
|
|
415
|
+
for (const member of flow.members) {
|
|
416
|
+
if (holdsBack(member.flags, member.name, flow.to)) {
|
|
417
|
+
for (const id of chains.declarationsOf(member)) {
|
|
418
|
+
found.push({ id, detail, site });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
403
422
|
}
|
|
404
423
|
return found;
|
|
405
424
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
import { isArrowFunction, isClassDeclaration, isClassExpression, isFunctionDeclaration, isFunctionExpression, isMethodDeclaration, isPropertyDeclaration, isVariableDeclaration, } from "@typescript/native/unstable/ast";
|
|
6
|
+
/** Whether one node is a function value written in place: an arrow or a function expression. */
|
|
7
|
+
function isFunctionValue(node) {
|
|
8
|
+
return node !== undefined && (isArrowFunction(node) || isFunctionExpression(node));
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The function one declaration declares: a function declaration and a class method with
|
|
12
|
+
* a body are their own; a variable and a class property whose initializer is an arrow or
|
|
13
|
+
* a function expression declare that function. Any other declaration declares none, an
|
|
14
|
+
* overload signature and an abstract or ambient method among them, because no body is
|
|
15
|
+
* there to read its parts.
|
|
16
|
+
*/
|
|
17
|
+
export function signatureOf(node) {
|
|
18
|
+
if (isFunctionDeclaration(node)) {
|
|
19
|
+
return node.body === undefined ? undefined : node;
|
|
20
|
+
}
|
|
21
|
+
if (isMethodDeclaration(node)) {
|
|
22
|
+
const owner = node.parent;
|
|
23
|
+
return node.body !== undefined && (isClassDeclaration(owner) || isClassExpression(owner))
|
|
24
|
+
? node
|
|
25
|
+
: undefined;
|
|
26
|
+
}
|
|
27
|
+
if ((isVariableDeclaration(node) || isPropertyDeclaration(node)) &&
|
|
28
|
+
isFunctionValue(node.initializer)) {
|
|
29
|
+
return node.initializer;
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
@@ -105,6 +105,14 @@ function readRecord(value, site, byReference, shape) {
|
|
|
105
105
|
}
|
|
106
106
|
return { records: named.map((one) => ({ ...written, bound: one.id })), refusals: [] };
|
|
107
107
|
}
|
|
108
|
+
function byReferenceOf(symbols) {
|
|
109
|
+
const byReference = new Map();
|
|
110
|
+
for (const symbol of symbols) {
|
|
111
|
+
const key = referenceKey(symbol.ref, symbol.position.path);
|
|
112
|
+
byReference.set(key, [...(byReference.get(key) ?? []), symbol]);
|
|
113
|
+
}
|
|
114
|
+
return byReference;
|
|
115
|
+
}
|
|
108
116
|
/** Reads one document at the target root, binding each record against the run's declarations. */
|
|
109
117
|
function readDocument(host, targetRoot, symbols, shape) {
|
|
110
118
|
const file = joinPath(targetRoot, shape.file);
|
|
@@ -139,11 +147,7 @@ function readDocument(host, targetRoot, symbols, shape) {
|
|
|
139
147
|
if ((description !== undefined && typeof description !== "string") || !Array.isArray(held)) {
|
|
140
148
|
return malformed(shape.file);
|
|
141
149
|
}
|
|
142
|
-
const byReference =
|
|
143
|
-
for (const symbol of symbols) {
|
|
144
|
-
const key = referenceKey(symbol.ref, symbol.position.path);
|
|
145
|
-
byReference.set(key, [...(byReference.get(key) ?? []), symbol]);
|
|
146
|
-
}
|
|
150
|
+
const byReference = byReferenceOf(symbols);
|
|
147
151
|
const records = [];
|
|
148
152
|
const refusals = [];
|
|
149
153
|
held.forEach((value, index) => {
|
|
@@ -172,9 +176,26 @@ export function readIgnoreFile(host, targetRoot, symbols) {
|
|
|
172
176
|
export function readBaseline(host, targetRoot, symbols) {
|
|
173
177
|
return readDocument(host, targetRoot, symbols, BASELINE_SHAPE);
|
|
174
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Rows a baseline write holds in memory, bound against the run's declarations as the same
|
|
181
|
+
* rows read back from the document would be, each carrying the reason given.
|
|
182
|
+
*/
|
|
183
|
+
export function bindRows(rows, reason, symbols) {
|
|
184
|
+
const byReference = byReferenceOf(symbols);
|
|
185
|
+
const site = documentSite(BASELINE_SHAPE);
|
|
186
|
+
const read = rows.map((row) => readRecord({ ...row, reason }, site, byReference, BASELINE_SHAPE));
|
|
187
|
+
return {
|
|
188
|
+
records: read.flatMap((one) => one.records),
|
|
189
|
+
refusals: read.flatMap((one) => one.refusals),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
175
192
|
/** What a written document says about itself, so the same report writes the same bytes. */
|
|
176
193
|
const BASELINE_DESCRIPTION = "Recorded findings: a run fails only on a finding this document does not hold. " +
|
|
177
194
|
"Every reason is the analyzer that recorded the row, never an adjudication.";
|
|
195
|
+
/** The reason every row a baseline write records carries. */
|
|
196
|
+
export function rowReason(provenance) {
|
|
197
|
+
return `recorded by ${provenance.analyzer} ${provenance.version}`;
|
|
198
|
+
}
|
|
178
199
|
/**
|
|
179
200
|
* A baseline holding one row per finding, in the order given, which is the report's. Every
|
|
180
201
|
* row carries the provenance as its reason. A finding missing a value a row needs, and an
|
|
@@ -185,7 +206,7 @@ export function writeBaseline(findings, provenance) {
|
|
|
185
206
|
if (provenance.analyzer === "" || provenance.version === "") {
|
|
186
207
|
throw new Error(`a baseline row would carry no reason: the analyzer identity names ${JSON.stringify(provenance.analyzer)} at version ${JSON.stringify(provenance.version)}`);
|
|
187
208
|
}
|
|
188
|
-
const reason =
|
|
209
|
+
const reason = rowReason(provenance);
|
|
189
210
|
const baseline = findings.map((found) => {
|
|
190
211
|
if (found.code === "" || found.symbol === "" || found.path === "") {
|
|
191
212
|
throw new Error(`a baseline row would carry no reason: a finding names code ${JSON.stringify(found.code)}, symbol ${JSON.stringify(found.symbol)} and path ${JSON.stringify(found.path)}`);
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { SyntaxKind } from "@typescript/native/unstable/ast";
|
|
7
7
|
import { nodeKey } from "./inventory.js";
|
|
8
8
|
import { byPosition, positionKey, renderPosition } from "./position.js";
|
|
9
|
+
import { signatureOf } from "./signatures.js";
|
|
9
10
|
import { SUPPRESSION_WITHOUT_REASON, SuppressionError, siteText, } from "./suppress.js";
|
|
10
11
|
/**
|
|
11
12
|
* The three expressions the grammar applies to the text of each line comment, in this
|
|
@@ -134,7 +135,8 @@ export function lineComments(file) {
|
|
|
134
135
|
/**
|
|
135
136
|
* The declarations of one file by the line each begins on, the line of its first token: a
|
|
136
137
|
* decorator, a modifier or the keyword. A variable's first token is its statement's
|
|
137
|
-
* keyword, and a variable written on a later line of the statement also begins there.
|
|
138
|
+
* keyword, and a variable written on a later line of the statement also begins there. A
|
|
139
|
+
* function also begins, for a directive, on the line of each of its parameters.
|
|
138
140
|
*/
|
|
139
141
|
function declarationLines(file, held, symbols) {
|
|
140
142
|
const below = new Map();
|
|
@@ -151,6 +153,11 @@ function declarationLines(file, held, symbols) {
|
|
|
151
153
|
const one = id === undefined ? undefined : symbols.get(id);
|
|
152
154
|
if (one !== undefined) {
|
|
153
155
|
add(node.pos, one);
|
|
156
|
+
// A parameter on a later line of a wrapped signature takes its directive on the
|
|
157
|
+
// line above it, inside the parameter list, and the directive binds the function.
|
|
158
|
+
for (const parameter of signatureOf(node)?.parameters ?? []) {
|
|
159
|
+
add(parameter.pos, one);
|
|
160
|
+
}
|
|
154
161
|
const list = node.parent;
|
|
155
162
|
if (node.kind === SyntaxKind.VariableDeclaration &&
|
|
156
163
|
list.kind === SyntaxKind.VariableDeclarationList &&
|
package/dist/src/suppress.js
CHANGED
|
@@ -79,6 +79,7 @@ const ROW_SUBJECTS = new Set([
|
|
|
79
79
|
"dependency",
|
|
80
80
|
"module-directive",
|
|
81
81
|
"root",
|
|
82
|
+
"configured-declaration",
|
|
82
83
|
"suppression",
|
|
83
84
|
"edge",
|
|
84
85
|
]);
|
|
@@ -86,6 +87,32 @@ const ROW_SUBJECTS = new Set([
|
|
|
86
87
|
export function isRowSubject(kind) {
|
|
87
88
|
return ROW_SUBJECTS.has(kind);
|
|
88
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* The subject kinds that are a part of a declaration rather than a declaration: a part
|
|
92
|
+
* has no reference of its own, so a finding about one names the declaration that holds
|
|
93
|
+
* it, and the part is decided inside that declaration whatever uses it.
|
|
94
|
+
*/
|
|
95
|
+
const PART_SUBJECTS = new Set([
|
|
96
|
+
"parameter",
|
|
97
|
+
"receiver",
|
|
98
|
+
"result",
|
|
99
|
+
"statement",
|
|
100
|
+
"case",
|
|
101
|
+
"store",
|
|
102
|
+
]);
|
|
103
|
+
/** Whether a finding's subject is a part of the declaration its reference names. */
|
|
104
|
+
export function isPartSubject(kind) {
|
|
105
|
+
return PART_SUBJECTS.has(kind);
|
|
106
|
+
}
|
|
107
|
+
/** The prefix of the codes of the part kinds, the intra-function range. */
|
|
108
|
+
const PART_KIND_PREFIX = "DS18";
|
|
109
|
+
/**
|
|
110
|
+
* Whether a code is a part kind, whose record withholds its finding and marks nothing:
|
|
111
|
+
* the record says the part is wanted, not that the declaration holding it is.
|
|
112
|
+
*/
|
|
113
|
+
export function isPartKind(code) {
|
|
114
|
+
return code.startsWith(PART_KIND_PREFIX);
|
|
115
|
+
}
|
|
89
116
|
/** One finding's identity: its code and the position of the thing it names. */
|
|
90
117
|
function claimKey(finding) {
|
|
91
118
|
return `${finding.code}\u0000${positionKey(finding.position)}`;
|
package/dist/src/value-reach.js
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
|
import { ObjectFlags, SymbolFlags, } from "@typescript/native/unstable/sync";
|
|
9
11
|
import { classNames, typesAt } from "./class-members.js";
|
|
@@ -11,6 +13,8 @@ import { classNames, typesAt } from "./class-members.js";
|
|
|
11
13
|
const WRITTEN = ObjectFlags.ObjectLiteral | ObjectFlags.Mapped;
|
|
12
14
|
/** The symbols of an anonymous object type the target writes as a type. */
|
|
13
15
|
const LITERAL_SYMBOLS = SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral;
|
|
16
|
+
/** The two conversion methods a serializer or a formatter resolves on a value's own shape. */
|
|
17
|
+
const CONVERSION_NAMES = new Set(["toJSON", "toString"]);
|
|
14
18
|
/** The global array types, whose element type is what an array value carries. */
|
|
15
19
|
function arrayTargets(project) {
|
|
16
20
|
const checker = project.checker;
|
|
@@ -73,6 +77,22 @@ export function valueReach(project, held) {
|
|
|
73
77
|
...checker.getIndexInfosOfType(type).map((info) => info.valueType),
|
|
74
78
|
];
|
|
75
79
|
};
|
|
80
|
+
/** Whether the walk reads one type's members because the target writes the type, not as a class. */
|
|
81
|
+
const writtenObject = (type) => {
|
|
82
|
+
if (!type.isObjectType() || classOf(type) !== undefined) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
const target = targetOf(type);
|
|
86
|
+
const isList = type.isTypeReference() &&
|
|
87
|
+
(arrays.has(target.id) ||
|
|
88
|
+
(target.isObjectType() && (target.objectFlags & ObjectFlags.Tuple) !== 0));
|
|
89
|
+
return !isList && isWritten(type);
|
|
90
|
+
};
|
|
91
|
+
/** The data members and conversion methods of one written object type. */
|
|
92
|
+
const carried = (type) => checker
|
|
93
|
+
.getPropertiesOfType(type)
|
|
94
|
+
.filter((property) => (property.flags & SymbolFlags.Property) !== 0 ||
|
|
95
|
+
((property.flags & SymbolFlags.Method) !== 0 && CONVERSION_NAMES.has(property.name)));
|
|
76
96
|
/** The types a value of one type holds, read once per type. */
|
|
77
97
|
const childrenOf = (type) => {
|
|
78
98
|
const known = children.get(type.id);
|
|
@@ -99,8 +119,9 @@ export function valueReach(project, held) {
|
|
|
99
119
|
return found;
|
|
100
120
|
};
|
|
101
121
|
return {
|
|
102
|
-
|
|
103
|
-
const
|
|
122
|
+
reachedFrom: (type) => {
|
|
123
|
+
const classes = new Set();
|
|
124
|
+
const members = [];
|
|
104
125
|
const seen = new Set();
|
|
105
126
|
const pending = [type];
|
|
106
127
|
for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
|
|
@@ -110,11 +131,14 @@ export function valueReach(project, held) {
|
|
|
110
131
|
seen.add(next.id);
|
|
111
132
|
const owner = classOf(next);
|
|
112
133
|
if (owner !== undefined) {
|
|
113
|
-
|
|
134
|
+
classes.add(owner);
|
|
135
|
+
}
|
|
136
|
+
else if (writtenObject(next)) {
|
|
137
|
+
members.push(...carried(next));
|
|
114
138
|
}
|
|
115
139
|
pending.push(...childrenOf(next));
|
|
116
140
|
}
|
|
117
|
-
return
|
|
141
|
+
return { classes, members };
|
|
118
142
|
},
|
|
119
143
|
};
|
|
120
144
|
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { runAnalysis } from "../analysis.js";
|
|
1
|
+
import { runAnalysis, runBaselineWrite } from "../analysis.js";
|
|
2
2
|
import { ConfigError } from "../config.js";
|
|
3
3
|
import { CONFORMANCE, DECLARED_GAPS } from "../conformance.js";
|
|
4
4
|
import { moduleIdentity } from "../consumers.js";
|
|
5
|
-
import { analyzerProvenance,
|
|
5
|
+
import { analyzerProvenance, verdictOf } from "../findings-pass.js";
|
|
6
6
|
import { joinPath, normalizePath, relativePath, resolvePath } from "../paths.js";
|
|
7
7
|
import { buildReport, capFindings, sortFindings, } from "../report.js";
|
|
8
8
|
import { json } from "../reporters/json.js";
|
|
9
9
|
import { RENDERINGS } from "../reporters/reporters.js";
|
|
10
10
|
import { parseTemplate, TemplateError } from "../reporters/template.js";
|
|
11
11
|
import { resolve } from "../resolve.js";
|
|
12
|
-
import { writeBaseline } from "../suppress-file.js";
|
|
12
|
+
import { rowReason, writeBaseline } from "../suppress-file.js";
|
|
13
13
|
import { CONTRACT_VERSION } from "../version.js";
|
|
14
14
|
import { EXIT_CLEAN, EXIT_FAILURE } from "./verb.js";
|
|
15
15
|
/** The options `analyze` takes beside the ones every verb takes. */
|
|
@@ -107,8 +107,9 @@ function templateOf(host, invoked, named, formats) {
|
|
|
107
107
|
/**
|
|
108
108
|
* Analyzes the scope under the configuration and writes the JSON report to the path
|
|
109
109
|
* `--report` names, each rendering `--format` names beside it at the report's path with the
|
|
110
|
-
* format's suffix appended, and
|
|
111
|
-
*
|
|
110
|
+
* format's suffix appended, and where `--baseline-write` names a path the baseline a fixpoint
|
|
111
|
+
* over the analysis records, the report then being its first round, which reads no
|
|
112
|
+
* baseline. The template format renders the template `--template` names. Nothing is written to
|
|
112
113
|
* the output stream; diagnostics go to the error stream and the verdict is the exit code, or 0
|
|
113
114
|
* under `--exit-code=off`, and a rendering that cannot be written ends the run with 3.
|
|
114
115
|
*/
|
|
@@ -149,9 +150,19 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
|
|
|
149
150
|
let verdict;
|
|
150
151
|
try {
|
|
151
152
|
const root = runRelative(invoked, targetRoot);
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
const mode = { production: true };
|
|
154
|
+
let analysis;
|
|
155
|
+
if (documents.baseline === undefined) {
|
|
156
|
+
analysis = runAnalysis(openClient(false), host, scoped, config, provenance, mode);
|
|
157
|
+
recorded = [];
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
const reason = rowReason(analyzerProvenance(version));
|
|
161
|
+
const write = runBaselineWrite(openClient(false), host, scoped, config, provenance, mode, reason);
|
|
162
|
+
analysis = write.first;
|
|
163
|
+
recorded = write.rows;
|
|
164
|
+
}
|
|
165
|
+
const { result, run } = analysis;
|
|
155
166
|
const built = buildReport({
|
|
156
167
|
contractVersion: CONTRACT_VERSION,
|
|
157
168
|
version,
|
|
@@ -167,9 +178,6 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
|
|
|
167
178
|
result,
|
|
168
179
|
testFileRules: run.testFileRules,
|
|
169
180
|
});
|
|
170
|
-
// The baseline records every finding, so its rows are taken before the cap bounds what
|
|
171
|
-
// a rendering prints: a baseline missing a reported finding fails the next run on it.
|
|
172
|
-
recorded = recordedFindings(result);
|
|
173
181
|
report = capFindings(sortFindings(built, config.reporters.sort), config.reporters.maxFindings);
|
|
174
182
|
verdict = verdictOf(result, config);
|
|
175
183
|
}
|
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
import { CONFORMANCE } from "../conformance.js";
|
|
2
|
-
import { ANALYZER_NAME, LANGUAGE,
|
|
2
|
+
import { ANALYZER_NAME, LANGUAGE, SCHEMA_VERSIONS_ACCEPTED } from "../report.js";
|
|
3
3
|
import { CONTRACT_VERSION } from "../version.js";
|
|
4
4
|
import { EXIT_CLEAN, EXIT_FAILURE, EXIT_USAGE } from "./verb.js";
|
|
5
5
|
/**
|
|
6
6
|
* The document `describe` writes: the analyzer, the Contract version it implements, the
|
|
7
7
|
* report schema versions it reads, the languages it claims, and its recorded result over
|
|
8
|
-
* the conformance corpus.
|
|
8
|
+
* the conformance corpus, a member the document omits where no run is recorded.
|
|
9
9
|
*/
|
|
10
10
|
export function describeDocument(version, recorded) {
|
|
11
11
|
const document = {
|
|
12
12
|
name: ANALYZER_NAME,
|
|
13
13
|
version,
|
|
14
14
|
contract_version: CONTRACT_VERSION,
|
|
15
|
-
schema_versions_accepted:
|
|
15
|
+
schema_versions_accepted: SCHEMA_VERSIONS_ACCEPTED,
|
|
16
16
|
languages: [LANGUAGE],
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
...(recorded === undefined
|
|
18
|
+
? {}
|
|
19
|
+
: {
|
|
20
|
+
conformance: {
|
|
21
|
+
corpus_version: recorded.corpusVersion,
|
|
22
|
+
result: recorded.result,
|
|
23
|
+
digest: recorded.digest,
|
|
24
|
+
},
|
|
25
|
+
}),
|
|
22
26
|
};
|
|
23
27
|
return `${JSON.stringify(document, null, 2)}\n`;
|
|
24
28
|
}
|
package/dist/src/version.js
CHANGED
package/jsr.json
CHANGED