@cplieger/deadset-ts 8.0.0-dev.6 → 8.0.0-dev.8
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/dist/bin/mapper-connection.js +2 -1
- package/dist/bin/node-host.js +24 -5
- package/dist/src/analysis.js +37 -56
- package/dist/src/automatic-types.js +10 -29
- package/dist/src/build-scripts.js +17 -29
- package/dist/src/component-layer.js +4 -2
- package/dist/src/components.js +2 -3
- package/dist/src/config.js +6 -82
- package/dist/src/configuration-files.js +27 -168
- package/dist/src/configured-declarations.js +15 -20
- package/dist/src/consumers.js +2 -2
- package/dist/src/contract/config.generated.js +544 -0
- package/dist/src/contract/contract.generated.js +13 -0
- package/dist/src/contract/report.generated.js +3 -2
- package/dist/src/conventions.js +20 -60
- package/dist/src/dependencies.js +25 -43
- package/dist/src/discover.js +14 -40
- package/dist/src/edges.js +38 -122
- package/dist/src/element-access.js +4 -6
- package/dist/src/entry-point-gaps.js +2 -8
- package/dist/src/entry-points.js +3 -10
- package/dist/src/enum-group.js +3 -10
- package/dist/src/erased-stores.js +4 -6
- package/dist/src/exempt-classes.js +0 -4
- package/dist/src/exempt.js +6 -22
- package/dist/src/file-facts.js +2 -25
- package/dist/src/findings/completion.js +5 -4
- package/dist/src/findings/emitters.js +1 -1
- package/dist/src/findings/intra-function.js +1 -1
- package/dist/src/findings/non-code-artifacts.js +2 -2
- package/dist/src/findings/reads-and-writes.js +2 -1
- package/dist/src/findings/self-check.js +3 -2
- package/dist/src/findings/unused-declarations.js +1 -1
- package/dist/src/findings/visibility-narrowing.js +2 -2
- package/dist/src/findings-pass.js +7 -12
- package/dist/src/framework-lifecycle.js +7 -7
- package/dist/src/fs/document.js +59 -0
- package/dist/src/fs/manifest.js +43 -0
- package/dist/src/fs/tree.js +88 -0
- package/dist/src/git-ignore.js +34 -25
- package/dist/src/html-entries.js +6 -33
- package/dist/src/in-operands.js +2 -1
- package/dist/src/injection-container.js +1 -1
- package/dist/src/intra-function-parts.js +10 -13
- package/dist/src/inventory.js +2 -21
- package/dist/src/json-document.js +198 -67
- package/dist/src/keyof-comparisons.js +4 -6
- package/dist/src/kinds.js +0 -7
- package/dist/src/manifest.js +5 -43
- package/dist/src/mdx-entries.js +3 -26
- package/dist/src/partial.js +2 -1
- package/dist/src/paths.js +26 -15
- package/dist/src/position.js +2 -1
- package/dist/src/print.js +11 -79
- package/dist/src/product.js +13 -0
- package/dist/src/references.js +14 -27
- package/dist/src/report-schema.js +3 -225
- package/dist/src/report.js +22 -31
- package/dist/src/reporters/sarif.js +4 -2
- package/dist/src/reporters/template.js +4 -19
- package/dist/src/resolve.js +245 -724
- package/dist/src/roots.js +7 -17
- package/dist/src/run.js +55 -61
- package/dist/src/schema.js +320 -343
- package/dist/src/scope.js +34 -103
- package/dist/src/serialization-contract.js +5 -9
- package/dist/src/session.js +2 -1
- package/dist/src/suppress-file.js +21 -25
- package/dist/src/suppress.js +3 -3
- package/dist/src/symbol-keys.js +3 -5
- package/dist/src/template-field.js +9 -19
- package/dist/src/test-globals.js +6 -28
- package/dist/src/test-joins.js +6 -6
- package/dist/src/text/compare.js +26 -0
- package/dist/src/text/glob.js +194 -0
- package/dist/src/text/shell.js +159 -0
- package/dist/src/{yaml-document.js → text/yaml.js} +38 -0
- package/dist/src/type-errors.js +14 -38
- package/dist/src/unseen-loads.js +2 -9
- package/dist/src/unwrap.js +23 -0
- package/dist/src/usage.js +10 -0
- package/dist/src/verbs/analyze.js +25 -26
- package/dist/src/verbs/describe.js +4 -4
- package/dist/src/verbs/explain.js +8 -6
- package/dist/src/verbs/print-projects.js +4 -3
- package/dist/src/verbs/print-retained.js +2 -1
- package/dist/src/verbs/print-roots.js +2 -1
- package/dist/src/verbs/version.js +2 -1
- package/dist/src/workflow-steps.js +9 -46
- package/dist/src/workspace-manifests.js +2 -2
- package/dist/src/workspace-programs.js +5 -3
- package/dist/src/workspace-resolution.js +13 -17
- package/dist/src/workspace.js +28 -116
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +55 -76
- package/src/automatic-types.ts +11 -29
- package/src/build-scripts.ts +17 -38
- package/src/component-layer.ts +4 -2
- package/src/components.ts +2 -4
- package/src/config.ts +31 -296
- package/src/configuration-files.ts +27 -170
- package/src/configured-declarations.ts +18 -23
- package/src/consumers.ts +4 -4
- package/src/contract/config.generated.ts +798 -0
- package/src/contract/contract.generated.ts +25 -0
- package/src/contract/report.generated.ts +3 -2
- package/src/conventions.ts +26 -74
- package/src/dependencies.ts +32 -50
- package/src/discover.ts +15 -40
- package/src/edges.ts +41 -140
- package/src/element-access.ts +4 -7
- package/src/entry-point-gaps.ts +3 -9
- package/src/entry-points.ts +2 -13
- package/src/enum-group.ts +2 -11
- package/src/erased-stores.ts +3 -7
- package/src/exempt-classes.ts +0 -5
- package/src/exempt.ts +8 -30
- package/src/file-facts.ts +6 -24
- package/src/findings/completion.ts +5 -6
- package/src/findings/emitter.ts +2 -2
- package/src/findings/emitters.ts +1 -1
- package/src/findings/intra-function.ts +1 -1
- package/src/findings/non-code-artifacts.ts +4 -4
- package/src/findings/reads-and-writes.ts +2 -1
- package/src/findings/self-check.ts +3 -2
- package/src/findings/unused-declarations.ts +1 -1
- package/src/findings/visibility-narrowing.ts +2 -2
- package/src/findings-pass.ts +9 -15
- package/src/framework-lifecycle.ts +7 -7
- package/src/fs/document.ts +74 -0
- package/src/fs/manifest.ts +52 -0
- package/src/fs/tree.ts +110 -0
- package/src/git-ignore.ts +38 -25
- package/src/host.ts +12 -5
- package/src/html-entries.ts +6 -33
- package/src/in-operands.ts +2 -1
- package/src/injection-container.ts +1 -1
- package/src/intra-function-parts.ts +10 -14
- package/src/inventory.ts +2 -20
- package/src/json-document.ts +241 -76
- package/src/keyof-comparisons.ts +3 -7
- package/src/kinds.ts +0 -8
- package/src/manifest.ts +5 -46
- package/src/mdx-entries.ts +3 -26
- package/src/partial.ts +2 -1
- package/src/paths.ts +27 -15
- package/src/position.ts +2 -1
- package/src/print.ts +12 -92
- package/src/product.ts +18 -0
- package/src/references.ts +14 -29
- package/src/report-schema.ts +3 -301
- package/src/report.ts +24 -34
- package/src/reporters/sarif.ts +4 -4
- package/src/reporters/template.ts +4 -20
- package/src/resolve.ts +283 -1114
- package/src/roots.ts +7 -20
- package/src/run.ts +57 -75
- package/src/schema.ts +391 -388
- package/src/scope.ts +40 -126
- package/src/serialization-contract.ts +5 -10
- package/src/session.ts +2 -1
- package/src/suppress-file.ts +21 -29
- package/src/suppress.ts +3 -3
- package/src/symbol-keys.ts +2 -5
- package/src/template-field.ts +15 -25
- package/src/test-globals.ts +9 -27
- package/src/test-joins.ts +6 -8
- package/src/text/compare.ts +27 -0
- package/src/text/glob.ts +210 -0
- package/src/text/shell.ts +161 -0
- package/src/{yaml-document.ts → text/yaml.ts} +46 -0
- package/src/type-errors.ts +25 -36
- package/src/unseen-loads.ts +1 -12
- package/src/unwrap.ts +34 -0
- package/src/usage.ts +10 -0
- package/src/verbs/analyze.ts +32 -42
- package/src/verbs/describe.ts +4 -4
- package/src/verbs/explain.ts +8 -10
- package/src/verbs/print-projects.ts +4 -3
- package/src/verbs/print-retained.ts +2 -1
- package/src/verbs/print-roots.ts +2 -1
- package/src/verbs/verb.ts +2 -2
- package/src/verbs/version.ts +2 -1
- package/src/workflow-steps.ts +9 -44
- package/src/workspace-manifests.ts +2 -2
- package/src/workspace-programs.ts +5 -3
- package/src/workspace-resolution.ts +21 -17
- package/src/workspace.ts +35 -121
- package/dist/src/glob.js +0 -65
- package/dist/src/pnpm-workspace.js +0 -120
- package/src/glob.ts +0 -67
- package/src/pnpm-workspace.ts +0 -135
package/src/edges.ts
CHANGED
|
@@ -5,15 +5,18 @@
|
|
|
5
5
|
* pairing is a use, and the merge reads the two evaluations together.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { EDGES_SCHEMA } from "./contract/config.generated.ts";
|
|
9
9
|
import type { Finding } from "./finding.ts";
|
|
10
10
|
import type { EdgeSide, EdgeSideName } from "./findings/boundary.ts";
|
|
11
|
+
import { readTargetDocument } from "./fs/document.ts";
|
|
11
12
|
import type { Host } from "./host.ts";
|
|
12
13
|
import type { InventorySymbol } from "./inventory.ts";
|
|
13
|
-
import { positionAt,
|
|
14
|
-
import { joinPath } from "./paths.ts";
|
|
14
|
+
import { dotted, placeIn, positionAt, readStrict, StrictJSONError } from "./json-document.ts";
|
|
15
15
|
import { positionKey } from "./position.ts";
|
|
16
|
+
import { firstRefusal } from "./schema.ts";
|
|
16
17
|
import { isPartSubject } from "./suppress.ts";
|
|
18
|
+
import { bytewise } from "./text/compare.ts";
|
|
19
|
+
import { UsageError } from "./usage.ts";
|
|
17
20
|
|
|
18
21
|
/** The name and the location the Contract fixes for the edges document. */
|
|
19
22
|
export const EDGES_FILE = "deadset-edges.json";
|
|
@@ -21,155 +24,61 @@ export const EDGES_FILE = "deadset-edges.json";
|
|
|
21
24
|
/** The language prefix of every reference this analyzer evaluates. */
|
|
22
25
|
const OWN_PREFIX = "ts://";
|
|
23
26
|
|
|
24
|
-
/** The closed key list of the document and of one edge. */
|
|
25
|
-
const DOCUMENT_KEYS: ReadonlySet<string> = new Set(["description", "edges"]);
|
|
26
|
-
const EDGE_KEYS: ReadonlySet<string> = new Set(["id", "because", "provides", "used_by"]);
|
|
27
|
-
|
|
28
|
-
/** An edge identifier: solidus-separated segments of ASCII letters, digits, `_`, `.` and `-`. */
|
|
29
|
-
const ID_FORM = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)*$/u;
|
|
30
|
-
|
|
31
|
-
/** A reference naming one symbol of some language: `<language>://<scope>#<fragment>`. */
|
|
32
|
-
const REFERENCE_FORM = /^[a-z][a-z0-9]*:\/\/[^ \t\r\n#]+#[^ \t\r\n]*$/u;
|
|
33
|
-
|
|
34
|
-
const DOCUMENT_WANT = "an object carrying the edges array, and an optional description";
|
|
35
|
-
const EDGE_WANT = "an edge naming id, provides and used_by, each a string, and an optional because";
|
|
36
|
-
const ID_WANT =
|
|
37
|
-
"one or more solidus-separated segments of ASCII letters, digits, underscore, full stop and hyphen";
|
|
38
|
-
const REFERENCE_WANT =
|
|
39
|
-
"a stable symbol reference naming one symbol exactly, <language>://<scope>#<fragment>";
|
|
40
|
-
|
|
41
27
|
/** An edges document the Contract refuses: a malformed input, answered with the usage code. */
|
|
42
|
-
class EdgesError extends
|
|
43
|
-
constructor(at: string,
|
|
44
|
-
super(
|
|
28
|
+
class EdgesError extends UsageError {
|
|
29
|
+
constructor(at: string, reason: string) {
|
|
30
|
+
super(`${at}: ${reason}`);
|
|
45
31
|
this.name = "EdgesError";
|
|
46
32
|
}
|
|
47
33
|
}
|
|
48
34
|
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
* would name a set of symbols rather than one.
|
|
52
|
-
*/
|
|
53
|
-
function carriesWildcard(ref: string): boolean {
|
|
54
|
-
let quoted = false;
|
|
55
|
-
let computed = false;
|
|
56
|
-
let escaped = false;
|
|
57
|
-
for (const char of ref) {
|
|
58
|
-
if (escaped) {
|
|
59
|
-
escaped = false;
|
|
60
|
-
} else if (quoted && char === "\\") {
|
|
61
|
-
escaped = true;
|
|
62
|
-
} else if (quoted) {
|
|
63
|
-
quoted = char !== "'";
|
|
64
|
-
} else if (computed) {
|
|
65
|
-
computed = char !== "]";
|
|
66
|
-
} else if (char === "'") {
|
|
67
|
-
quoted = true;
|
|
68
|
-
} else if (char === "[") {
|
|
69
|
-
computed = true;
|
|
70
|
-
} else if (char === "*") {
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function isObject(value: unknown): value is Record<string, unknown> {
|
|
78
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** One edge, checked: the closed key list, the value types and the value forms. */
|
|
82
|
-
function readEdge(value: unknown, at: string): { id: string; provides: string; usedBy: string } {
|
|
83
|
-
const refuse = (text: string, want: string): never => {
|
|
84
|
-
throw new EdgesError(at, text, want);
|
|
85
|
-
};
|
|
86
|
-
if (!isObject(value)) {
|
|
87
|
-
return refuse(JSON.stringify(value), EDGE_WANT);
|
|
88
|
-
}
|
|
89
|
-
for (const [key, held] of Object.entries(value)) {
|
|
90
|
-
if (!EDGE_KEYS.has(key) || typeof held !== "string") {
|
|
91
|
-
refuse(`the member ${JSON.stringify(key)} holding ${JSON.stringify(held)}`, EDGE_WANT);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
const edge = value as Readonly<Record<string, string | undefined>>;
|
|
95
|
-
const id = edge["id"] ?? refuse("an edge naming no id", EDGE_WANT);
|
|
96
|
-
const provides = edge["provides"] ?? refuse("an edge naming no provides side", EDGE_WANT);
|
|
97
|
-
const usedBy = edge["used_by"] ?? refuse("an edge naming no used_by side", EDGE_WANT);
|
|
98
|
-
if (!ID_FORM.test(id)) {
|
|
99
|
-
refuse(JSON.stringify(id), ID_WANT);
|
|
100
|
-
}
|
|
101
|
-
for (const ref of [provides, usedBy]) {
|
|
102
|
-
if (!REFERENCE_FORM.test(ref) || carriesWildcard(ref)) {
|
|
103
|
-
refuse(JSON.stringify(ref), REFERENCE_WANT);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return { id, provides, usedBy };
|
|
107
|
-
}
|
|
35
|
+
/** The sides of one edge, in the order a reader yields them. */
|
|
36
|
+
const SIDE_NAMES: readonly EdgeSideName[] = ["provides", "used_by"];
|
|
108
37
|
|
|
109
38
|
/**
|
|
110
39
|
* Every side of a declared edge that names a symbol of this language, in document order,
|
|
111
40
|
* `provides` before `used_by` within an edge. An absent document declares no edge. The
|
|
112
|
-
* document is strict JSON
|
|
113
|
-
* {@link EdgesError}.
|
|
41
|
+
* document is strict JSON held to the Contract's edges schema, and a defect in it is an
|
|
42
|
+
* {@link EdgesError} at the line and column of the edge it is in, where it is in one.
|
|
114
43
|
*/
|
|
115
44
|
export function readEdgeSides(host: Host, targetRoot: string): readonly EdgeSide[] {
|
|
116
|
-
const
|
|
117
|
-
if (
|
|
45
|
+
const text = readTargetDocument(host, targetRoot, EDGES_FILE);
|
|
46
|
+
if (text === undefined) {
|
|
118
47
|
return [];
|
|
119
48
|
}
|
|
120
|
-
|
|
121
|
-
let parsed: unknown;
|
|
49
|
+
let read;
|
|
122
50
|
try {
|
|
123
|
-
|
|
51
|
+
read = readStrict(text, "edges");
|
|
124
52
|
} catch (error: unknown) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
if (!isObject(parsed)) {
|
|
132
|
-
throw new EdgesError(EDGES_FILE, JSON.stringify(parsed), DOCUMENT_WANT);
|
|
133
|
-
}
|
|
134
|
-
const walk = walkDocument(text, "edges");
|
|
135
|
-
if (walk.twice !== undefined) {
|
|
136
|
-
throw new EdgesError(
|
|
137
|
-
positionKey(positionAt(text, EDGES_FILE, walk.twice.offset)),
|
|
138
|
-
`the member ${JSON.stringify(walk.twice.name)} is written twice, so neither value is chosen`,
|
|
139
|
-
DOCUMENT_WANT,
|
|
140
|
-
);
|
|
53
|
+
if (!(error instanceof StrictJSONError)) {
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
throw new EdgesError(positionKey(positionAt(text, EDGES_FILE, error.offset)), error.message);
|
|
141
57
|
}
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
throw new EdgesError(
|
|
151
|
-
EDGES_FILE,
|
|
152
|
-
unknown === undefined ? EDGES_FILE : `the member ${JSON.stringify(unknown)}`,
|
|
153
|
-
DOCUMENT_WANT,
|
|
154
|
-
);
|
|
58
|
+
const value = read.value;
|
|
59
|
+
const refused = firstRefusal(EDGES_SCHEMA, value);
|
|
60
|
+
if (refused !== undefined) {
|
|
61
|
+
const place = placeIn(value, refused.pointer, refused.member);
|
|
62
|
+
const offset = place.length > 1 ? read.starts.get(place[1] ?? "") : undefined;
|
|
63
|
+
const at =
|
|
64
|
+
offset === undefined ? EDGES_FILE : positionKey(positionAt(text, EDGES_FILE, offset));
|
|
65
|
+
const where = dotted(place);
|
|
66
|
+
throw new EdgesError(at, where === "" ? refused.reason : `${where}: ${refused.reason}`);
|
|
155
67
|
}
|
|
68
|
+
const edges = (
|
|
69
|
+
value as { readonly edges: Readonly<Record<string, Record<EdgeSideName, string>>> }
|
|
70
|
+
).edges;
|
|
156
71
|
|
|
157
72
|
const sides: EdgeSide[] = [];
|
|
158
|
-
(
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
["provides", edge.provides],
|
|
165
|
-
["used_by", edge.usedBy],
|
|
166
|
-
];
|
|
167
|
-
for (const [side, symbol] of named) {
|
|
168
|
-
if (symbol.startsWith(OWN_PREFIX)) {
|
|
169
|
-
sides.push({ edge: edge.id, side, symbol });
|
|
73
|
+
for (const id of read.starts.keys()) {
|
|
74
|
+
const edge = String(id);
|
|
75
|
+
for (const side of SIDE_NAMES) {
|
|
76
|
+
const symbol = edges[edge]?.[side];
|
|
77
|
+
if (symbol?.startsWith(OWN_PREFIX) === true) {
|
|
78
|
+
sides.push({ edge, side, symbol });
|
|
170
79
|
}
|
|
171
80
|
}
|
|
172
|
-
}
|
|
81
|
+
}
|
|
173
82
|
return sides;
|
|
174
83
|
}
|
|
175
84
|
|
|
@@ -190,14 +99,6 @@ interface Evaluated<F extends Finding> {
|
|
|
190
99
|
readonly evaluations: readonly EdgeEvaluation<F>[];
|
|
191
100
|
}
|
|
192
101
|
|
|
193
|
-
/** Two strings ordered bytewise. */
|
|
194
|
-
function compare(a: string, b: string): number {
|
|
195
|
-
if (a === b) {
|
|
196
|
-
return 0;
|
|
197
|
-
}
|
|
198
|
-
return a < b ? -1 : 1;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
102
|
/**
|
|
202
103
|
* Evaluates every declared side of this language over the run's findings. A side naming a
|
|
203
104
|
* symbol the run enumerates nothing under is `absent`; otherwise it is evaluated once per
|
|
@@ -242,6 +143,6 @@ export function evaluateEdges<F extends Finding>(
|
|
|
242
143
|
return { ...side, state: "dead", finding };
|
|
243
144
|
});
|
|
244
145
|
});
|
|
245
|
-
evaluations.sort((a, b) =>
|
|
146
|
+
evaluations.sort((a, b) => bytewise(a.edge, b.edge) || bytewise(a.side, b.side));
|
|
246
147
|
return { findings: findings.filter((_finding, at) => !pending.has(at)), evaluations };
|
|
247
148
|
}
|
package/src/element-access.ts
CHANGED
|
@@ -47,6 +47,7 @@ import { renderPosition } from "./position.ts";
|
|
|
47
47
|
import { UNANSWERED } from "./query.ts";
|
|
48
48
|
import type { Reference } from "./references.ts";
|
|
49
49
|
import type { ProjectView } from "./session.ts";
|
|
50
|
+
import { parenthesesRemoved } from "./unwrap.ts";
|
|
50
51
|
|
|
51
52
|
type Use = "read" | "write" | "both";
|
|
52
53
|
|
|
@@ -93,13 +94,9 @@ function useOf(node: Node): Use {
|
|
|
93
94
|
return "read";
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
function unwrapped(node: Expression): Expression {
|
|
97
|
-
return isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
97
|
/** Whether an index is a literal the checker resolves to a member by its own text. */
|
|
101
98
|
function isLiteralIndex(node: Expression): boolean {
|
|
102
|
-
const at =
|
|
99
|
+
const at = parenthesesRemoved(node);
|
|
103
100
|
return isStringLiteral(at) || isNumericLiteral(at) || isNoSubstitutionTemplateLiteral(at);
|
|
104
101
|
}
|
|
105
102
|
|
|
@@ -293,7 +290,7 @@ export function elementAccessReferences<Brand>(
|
|
|
293
290
|
const parameterOf = new Map<Candidate, { callable: Callable; at: number }>();
|
|
294
291
|
const byCallable = new Map<Identifier, Candidate[]>();
|
|
295
292
|
for (const candidate of candidates) {
|
|
296
|
-
const index =
|
|
293
|
+
const index = parenthesesRemoved(candidate.access.argumentExpression);
|
|
297
294
|
const fn = isIdentifier(index) ? enclosingFunction(candidate.access) : undefined;
|
|
298
295
|
const callable = fn === undefined ? undefined : callableOf(fn);
|
|
299
296
|
if (callable === undefined || !isIdentifier(index)) {
|
|
@@ -353,7 +350,7 @@ export function elementAccessReferences<Brand>(
|
|
|
353
350
|
continue;
|
|
354
351
|
}
|
|
355
352
|
// An object that is a parameter too is read as the argument each call passes there.
|
|
356
|
-
const object =
|
|
353
|
+
const object = parenthesesRemoved(candidate.access.expression);
|
|
357
354
|
const objectAt = isIdentifier(object)
|
|
358
355
|
? deferred.callable.parameters.findIndex(
|
|
359
356
|
(parameter) => isIdentifier(parameter.name) && parameter.name.text === object.text,
|
package/src/entry-point-gaps.ts
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* one at a time: beside a finding, or in a report's declared gaps.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { bytewise } from "./text/compare.ts";
|
|
12
|
+
|
|
11
13
|
/** One declined entry-point convention. */
|
|
12
14
|
export interface DeclinedConvention {
|
|
13
15
|
/** The tool the convention belongs to, by the name of the package or service. */
|
|
@@ -323,16 +325,8 @@ const PARTLY_ENTERED: readonly DeclinedConvention[] = [
|
|
|
323
325
|
},
|
|
324
326
|
];
|
|
325
327
|
|
|
326
|
-
/** Two strings ordered bytewise. */
|
|
327
|
-
function compare(a: string, b: string): number {
|
|
328
|
-
if (a === b) {
|
|
329
|
-
return 0;
|
|
330
|
-
}
|
|
331
|
-
return a < b ? -1 : 1;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
328
|
/** Every declined entry-point convention, ordered by tool and then by convention. */
|
|
335
329
|
export const DECLINED_CONVENTIONS: readonly DeclinedConvention[] = [
|
|
336
330
|
...UNREAD_TOOLS.map((tool) => ({ tool, convention: EVERY_CONVENTION, reason: UNREAD })),
|
|
337
331
|
...PARTLY_ENTERED,
|
|
338
|
-
].sort((a, b) =>
|
|
332
|
+
].sort((a, b) => bytewise(a.tool, b.tool) || bytewise(a.convention, b.convention));
|
package/src/entry-points.ts
CHANGED
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
15
|
isArrayLiteralExpression,
|
|
16
|
-
isAsExpression,
|
|
17
16
|
isCallExpression,
|
|
18
17
|
isExportAssignment,
|
|
19
18
|
isIdentifier,
|
|
@@ -21,10 +20,8 @@ import {
|
|
|
21
20
|
isNewExpression,
|
|
22
21
|
isNoSubstitutionTemplateLiteral,
|
|
23
22
|
isObjectLiteralExpression,
|
|
24
|
-
isParenthesizedExpression,
|
|
25
23
|
isPropertyAccessExpression,
|
|
26
24
|
isPropertyAssignment,
|
|
27
|
-
isSatisfiesExpression,
|
|
28
25
|
isStringLiteral,
|
|
29
26
|
SyntaxKind,
|
|
30
27
|
type Node,
|
|
@@ -39,7 +36,6 @@ import {
|
|
|
39
36
|
} from "./configuration-files.ts";
|
|
40
37
|
import type { Selected } from "./conventions.ts";
|
|
41
38
|
import { buildScriptEntries } from "./build-scripts.ts";
|
|
42
|
-
import { globExpression } from "./glob.ts";
|
|
43
39
|
import type { Host } from "./host.ts";
|
|
44
40
|
import { htmlEntries } from "./html-entries.ts";
|
|
45
41
|
import { documentEntries } from "./mdx-entries.ts";
|
|
@@ -49,6 +45,8 @@ import type { ProjectView } from "./session.ts";
|
|
|
49
45
|
import type { SourceFiles } from "./source-files.ts";
|
|
50
46
|
import { computedLoadDirectory, leadingLiteral } from "./unseen-loads.ts";
|
|
51
47
|
import { workflowEntries } from "./workflow-steps.ts";
|
|
48
|
+
import { unwrapped } from "./unwrap.ts";
|
|
49
|
+
import { globExpression } from "./text/glob.ts";
|
|
52
50
|
|
|
53
51
|
/** Which rule entered a file. */
|
|
54
52
|
export type EntryRule =
|
|
@@ -166,15 +164,6 @@ function literalOf(node: Node | undefined): string | undefined {
|
|
|
166
164
|
return undefined;
|
|
167
165
|
}
|
|
168
166
|
|
|
169
|
-
/** The expression a wrapper that changes no value holds: parentheses and type assertions. */
|
|
170
|
-
function unwrapped(node: Node): Node {
|
|
171
|
-
let at = node;
|
|
172
|
-
while (isParenthesizedExpression(at) || isAsExpression(at) || isSatisfiesExpression(at)) {
|
|
173
|
-
at = at.expression;
|
|
174
|
-
}
|
|
175
|
-
return at;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
167
|
/**
|
|
179
168
|
* The object literals one configuration file's default export is written as: the
|
|
180
169
|
* literal itself, or each literal argument of the call it is, at any depth of calls.
|
package/src/enum-group.ts
CHANGED
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
isIdentifier,
|
|
19
19
|
isMethodDeclaration,
|
|
20
20
|
isNoSubstitutionTemplateLiteral,
|
|
21
|
-
isParenthesizedExpression,
|
|
22
21
|
isParenthesizedTypeNode,
|
|
23
22
|
isPropertyAccessExpression,
|
|
24
23
|
isReturnStatement,
|
|
@@ -28,6 +27,7 @@ import {
|
|
|
28
27
|
type Node,
|
|
29
28
|
type SourceFile,
|
|
30
29
|
} from "@typescript/native/ast";
|
|
30
|
+
import { parenthesesRemoved } from "./unwrap.ts";
|
|
31
31
|
import { TypeFlags, type Type } from "@typescript/native/sync";
|
|
32
32
|
import { aliasChains } from "./alias-chain.ts";
|
|
33
33
|
import type { DetectorInput, Evidence } from "./exempt.ts";
|
|
@@ -55,15 +55,6 @@ interface Site {
|
|
|
55
55
|
readonly at: Node;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
/** The expression one node is, through however many parentheses are written around it. */
|
|
59
|
-
function unparenthesized(node: Node): Node {
|
|
60
|
-
let held = node;
|
|
61
|
-
while (isParenthesizedExpression(held)) {
|
|
62
|
-
held = held.expression;
|
|
63
|
-
}
|
|
64
|
-
return held;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
58
|
/**
|
|
68
59
|
* The name node a type node names its declaration by: the identifier of a type
|
|
69
60
|
* reference, the right side of a qualified one, through parentheses. A type that is no
|
|
@@ -86,7 +77,7 @@ function typeName(type: Node | undefined): Node | undefined {
|
|
|
86
77
|
|
|
87
78
|
/** The name node an expression names an object by: an identifier, or a property access's name. */
|
|
88
79
|
function objectName(expression: Node): Node | undefined {
|
|
89
|
-
const held =
|
|
80
|
+
const held = parenthesesRemoved(expression);
|
|
90
81
|
if (isIdentifier(held)) {
|
|
91
82
|
return held;
|
|
92
83
|
}
|
package/src/erased-stores.ts
CHANGED
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
isNoSubstitutionTemplateLiteral,
|
|
17
17
|
isNumericLiteral,
|
|
18
18
|
isObjectLiteralExpression,
|
|
19
|
-
isParenthesizedExpression,
|
|
20
19
|
isPropertyAccessExpression,
|
|
21
20
|
isPropertyAssignment,
|
|
22
21
|
isShorthandPropertyAssignment,
|
|
@@ -31,6 +30,7 @@ import { SymbolFlags, TypeFlags, type Type } from "@typescript/native/sync";
|
|
|
31
30
|
import { must } from "./query.ts";
|
|
32
31
|
import type { ProjectView } from "./session.ts";
|
|
33
32
|
import { checkedTypes } from "./value-reach.ts";
|
|
33
|
+
import { parenthesesRemoved } from "./unwrap.ts";
|
|
34
34
|
|
|
35
35
|
/** The types that keep nothing of the value they are given. */
|
|
36
36
|
const ERASED = TypeFlags.Any | TypeFlags.Unknown;
|
|
@@ -81,10 +81,6 @@ function literalKey(node: Expression): string | undefined {
|
|
|
81
81
|
: undefined;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
function unwrapped(node: Expression): Expression {
|
|
85
|
-
return isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
84
|
/**
|
|
89
85
|
* Whether a variable declaration's type can differ from what is stored in it: it carries an
|
|
90
86
|
* annotation, or it has neither an annotation nor an initializer, so its type evolves.
|
|
@@ -150,8 +146,8 @@ function propertyAt(source: Expression, name: string): Expression | undefined {
|
|
|
150
146
|
* here, and a default value is stored in its target too.
|
|
151
147
|
*/
|
|
152
148
|
function patternTargets(pattern: Expression, from: Stored, out: Target[]): void {
|
|
153
|
-
const target =
|
|
154
|
-
const source = from.path.length === 0 ?
|
|
149
|
+
const target = parenthesesRemoved(pattern);
|
|
150
|
+
const source = from.path.length === 0 ? parenthesesRemoved(from.value) : from.value;
|
|
155
151
|
const into = (element: Expression, step: Step, literal: Expression | undefined): void => {
|
|
156
152
|
patternTargets(
|
|
157
153
|
element,
|
package/src/exempt-classes.ts
CHANGED
|
@@ -47,8 +47,3 @@ const VISIBILITY: ReadonlyMap<TSExemptionClass, TypeScriptVisibility> = new Map(
|
|
|
47
47
|
export function typescriptVisibilityOf(exemptionClass: TSExemptionClass): TypeScriptVisibility {
|
|
48
48
|
return VISIBILITY.get(exemptionClass) ?? { private: false, privateName: false };
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
/** Whether one name is a class of the vocabulary, whichever language it runs on. */
|
|
52
|
-
export function isExemptionClass(name: string): name is ExemptionClass {
|
|
53
|
-
return EXEMPTION_CLASSES.some((row) => row.class === name);
|
|
54
|
-
}
|
package/src/exempt.ts
CHANGED
|
@@ -8,10 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { EXEMPTION_CLASSES } from "./contract/contract.generated.ts";
|
|
11
|
-
import type {
|
|
12
|
-
import { ConfigError } from "./config.ts";
|
|
11
|
+
import type { ResolvedConfig } from "./config.ts";
|
|
13
12
|
import {
|
|
14
|
-
isExemptionClass,
|
|
15
13
|
TS_EXEMPTION_CLASSES,
|
|
16
14
|
typescriptVisibilityOf,
|
|
17
15
|
type ExemptionClass,
|
|
@@ -25,6 +23,7 @@ import { Unanswerable } from "./query.ts";
|
|
|
25
23
|
import type { ProjectView } from "./session.ts";
|
|
26
24
|
import type { Templates } from "./template-field.ts";
|
|
27
25
|
import { sweep, type Exemption, type Implementing, type Mode, type SweepInput } from "./sweep.ts";
|
|
26
|
+
import { bytewise } from "./text/compare.ts";
|
|
28
27
|
|
|
29
28
|
/** One declaration a detector holds back, and where and why. */
|
|
30
29
|
export interface Evidence {
|
|
@@ -49,7 +48,7 @@ export interface DetectorInput<Brand> {
|
|
|
49
48
|
/** The configured templates, read once for the run. */
|
|
50
49
|
readonly templates: Templates;
|
|
51
50
|
/** The `ts` section, whose declarations the configured classes read. */
|
|
52
|
-
readonly ts:
|
|
51
|
+
readonly ts: ResolvedConfig["ts"];
|
|
53
52
|
/** The absolute path of each declared consumer, whose modules are part of the analysis. */
|
|
54
53
|
readonly consumers: readonly string[];
|
|
55
54
|
/** The project's generated files, by path below the target root, each with the detail its retentions name. */
|
|
@@ -73,24 +72,11 @@ interface Holding {
|
|
|
73
72
|
}
|
|
74
73
|
|
|
75
74
|
/**
|
|
76
|
-
* The classes `exemptions.disabled` switches off. A
|
|
77
|
-
*
|
|
78
|
-
* switches nothing off here, so one configuration serves every analyzer.
|
|
75
|
+
* The classes `exemptions.disabled` switches off. A class that runs on another language
|
|
76
|
+
* only switches nothing off here, so one configuration serves every analyzer.
|
|
79
77
|
*/
|
|
80
|
-
export function disabledClasses(config:
|
|
81
|
-
|
|
82
|
-
for (const name of config.exemptionsDisabled) {
|
|
83
|
-
if (!isExemptionClass(name)) {
|
|
84
|
-
const known = EXEMPTION_CLASSES.map((row) => row.class).join(", ");
|
|
85
|
-
throw new ConfigError(
|
|
86
|
-
"malformed",
|
|
87
|
-
"exemptions.disabled",
|
|
88
|
-
`exemptions.disabled: ${JSON.stringify(name)} is not an exemption class: the classes are ${known}`,
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
disabled.add(name);
|
|
92
|
-
}
|
|
93
|
-
return disabled;
|
|
78
|
+
export function disabledClasses(config: ResolvedConfig): ReadonlySet<ExemptionClass> {
|
|
79
|
+
return new Set(config.exemptions.disabled);
|
|
94
80
|
}
|
|
95
81
|
|
|
96
82
|
/**
|
|
@@ -221,14 +207,6 @@ export function heldRecords(records: readonly Exemption[], holding: Holding): De
|
|
|
221
207
|
return { records: exemptionsOf(found), inTestFiles };
|
|
222
208
|
}
|
|
223
209
|
|
|
224
|
-
/** Two strings ordered bytewise. */
|
|
225
|
-
function compare(a: string, b: string): number {
|
|
226
|
-
if (a === b) {
|
|
227
|
-
return 0;
|
|
228
|
-
}
|
|
229
|
-
return a < b ? -1 : 1;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
210
|
/**
|
|
233
211
|
* Records as a run holds them: ordered by site, then class, then declaration, and one
|
|
234
212
|
* per declaration, class, detail and file it holds while live, the one at the first
|
|
@@ -236,7 +214,7 @@ function compare(a: string, b: string): number {
|
|
|
236
214
|
*/
|
|
237
215
|
export function exemptionsOf(found: readonly Exemption[]): readonly Exemption[] {
|
|
238
216
|
const ordered = [...found].sort(
|
|
239
|
-
(a, b) => byPosition(a.site, b.site) ||
|
|
217
|
+
(a, b) => byPosition(a.site, b.site) || bytewise(a.class, b.class) || bytewise(a.id, b.id),
|
|
240
218
|
);
|
|
241
219
|
const seen = new Set<string>();
|
|
242
220
|
return ordered.filter((record) => {
|
package/src/file-facts.ts
CHANGED
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { SourceFile } from "@typescript/native/ast";
|
|
8
|
-
import { IGNORED_DIRECTORIES } from "./discover.ts";
|
|
9
8
|
import type { Host } from "./host.ts";
|
|
10
9
|
import { packageScope } from "./inventory.ts";
|
|
11
10
|
import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
|
|
12
11
|
import { renderRef } from "./ref.ts";
|
|
13
12
|
import type { ProjectView } from "./session.ts";
|
|
14
13
|
import type { UnseenLoad } from "./unseen-loads.ts";
|
|
14
|
+
import { filesBelow } from "./fs/tree.ts";
|
|
15
15
|
|
|
16
16
|
/** A file name the compiler reads as a source file. */
|
|
17
17
|
const SOURCE_FILE = /\.(?:ts|tsx|mts|cts)$/u;
|
|
@@ -74,29 +74,11 @@ function lastLine(text: string): number {
|
|
|
74
74
|
* that cannot be read is skipped, as discovery skips it.
|
|
75
75
|
*/
|
|
76
76
|
export function sourcePaths(host: Host, targetRoot: string): readonly string[] {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
entries = host.readDirectory(joinPath(targetRoot, dir));
|
|
83
|
-
} catch {
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
for (const entry of entries) {
|
|
87
|
-
const path = dir === "" ? entry.name : `${dir}/${entry.name}`;
|
|
88
|
-
if (entry.directory) {
|
|
89
|
-
if (!IGNORED_DIRECTORIES.has(entry.name)) {
|
|
90
|
-
pending.push(path);
|
|
91
|
-
}
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
if (SOURCE_FILE.test(entry.name) && !DECLARATION_FILE.test(entry.name)) {
|
|
95
|
-
found.push(path);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return found.sort((a, b) => (a < b ? -1 : 1));
|
|
77
|
+
return filesBelow(
|
|
78
|
+
host,
|
|
79
|
+
targetRoot,
|
|
80
|
+
(name) => SOURCE_FILE.test(name) && !DECLARATION_FILE.test(name),
|
|
81
|
+
);
|
|
100
82
|
}
|
|
101
83
|
|
|
102
84
|
/**
|
|
@@ -65,10 +65,9 @@ function everyDeclaredLoaded(boundary: Boundary): boolean {
|
|
|
65
65
|
* naming its family, else the code's default.
|
|
66
66
|
*/
|
|
67
67
|
function severityOf(input: EmitterInput, code: string, fallback: Severity): Severity {
|
|
68
|
-
const {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
);
|
|
68
|
+
const { severity } = input.config;
|
|
69
|
+
const key = [code, code.slice(0, FAMILY_KEY_LENGTH)].find((one) => Object.hasOwn(severity, one));
|
|
70
|
+
return key === undefined ? fallback : (severity[key] ?? fallback);
|
|
72
71
|
}
|
|
73
72
|
|
|
74
73
|
/**
|
|
@@ -225,7 +224,7 @@ function rootsCeiling(
|
|
|
225
224
|
): Confidence {
|
|
226
225
|
const unreachable = candidate?.relation === "reachability" || finding.unreachableOnly === true;
|
|
227
226
|
const unseen = new Set((input.files.unseenLoads ?? []).map((load) => load.configuration));
|
|
228
|
-
const complete = input.config.ts.
|
|
227
|
+
const complete = input.config.ts.roots_complete && !configurations.some((one) => unseen.has(one));
|
|
229
228
|
return unreachable && !complete ? "probable" : "certain";
|
|
230
229
|
}
|
|
231
230
|
|
|
@@ -585,7 +584,7 @@ export function dialsOf(
|
|
|
585
584
|
input: EmitterInput,
|
|
586
585
|
findings: readonly CompletedFinding[],
|
|
587
586
|
): Dials<CompletedFinding> {
|
|
588
|
-
const minConfidence = input.config.analysis.
|
|
587
|
+
const minConfidence = input.config.analysis.min_confidence;
|
|
589
588
|
const components = new Set(
|
|
590
589
|
findings
|
|
591
590
|
.filter(
|
package/src/findings/emitter.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RunSweep } from "../analysis.ts";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ResolvedConfig } from "../config.ts";
|
|
3
3
|
import type { Dependencies } from "../dependencies.ts";
|
|
4
4
|
import type { FileFacts } from "../file-facts.ts";
|
|
5
5
|
import type { Finding } from "../finding.ts";
|
|
@@ -14,7 +14,7 @@ import type { SelfCheckFacts } from "./self-check.ts";
|
|
|
14
14
|
* the facts about the run each emitter reads beside the sweep.
|
|
15
15
|
*/
|
|
16
16
|
export interface EmitterInput {
|
|
17
|
-
readonly config:
|
|
17
|
+
readonly config: ResolvedConfig;
|
|
18
18
|
/** The production sweep a report is built from, beside the matrix it judged. */
|
|
19
19
|
readonly swept: RunSweep;
|
|
20
20
|
/** The declarations carrying the deprecation marker in any configuration of the run. */
|
package/src/findings/emitters.ts
CHANGED
|
@@ -145,7 +145,7 @@ function atPossible(input: EmitterInput): EmitterInput {
|
|
|
145
145
|
const { config } = input;
|
|
146
146
|
return {
|
|
147
147
|
...input,
|
|
148
|
-
config: { ...config, analysis: { ...config.analysis,
|
|
148
|
+
config: { ...config, analysis: { ...config.analysis, min_confidence: "possible" } },
|
|
149
149
|
};
|
|
150
150
|
}
|
|
151
151
|
|
|
@@ -127,7 +127,7 @@ export const intraFunction: Emitter = (input) => {
|
|
|
127
127
|
}),
|
|
128
128
|
);
|
|
129
129
|
const { declared, loaded } = input.boundary.consumers;
|
|
130
|
-
const closed = input.config.
|
|
130
|
+
const closed = input.config.consumers.complete && declared.every((id) => loaded.includes(id));
|
|
131
131
|
const published = (id: string): boolean => {
|
|
132
132
|
const symbol = byId.get(id);
|
|
133
133
|
return (
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CODE } from "../contract/contract.generated.ts";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ResolvedConfig } from "../config.ts";
|
|
3
3
|
import type { Finding } from "../finding.ts";
|
|
4
4
|
import type { Emitter, EmitterInput } from "./emitter.ts";
|
|
5
5
|
|
|
@@ -15,10 +15,10 @@ const FILE_START = 1;
|
|
|
15
15
|
* completeness and listing no project declares it of a matrix the run derived, and a
|
|
16
16
|
* derived matrix is never complete.
|
|
17
17
|
*/
|
|
18
|
-
function completeMatrix(config:
|
|
18
|
+
function completeMatrix(config: ResolvedConfig): boolean {
|
|
19
19
|
return (
|
|
20
|
-
config.analysis.
|
|
21
|
-
config.analysis.configurations.some((entry) =>
|
|
20
|
+
config.analysis.matrix.complete &&
|
|
21
|
+
config.analysis.configurations.some((entry) => "project" in entry)
|
|
22
22
|
);
|
|
23
23
|
}
|
|
24
24
|
|