@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/scope.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SCOPE_SCHEMA } from "./contract/config.generated.ts";
|
|
2
2
|
import type { Host } from "./host.ts";
|
|
3
|
+
import { dotted, locatedFault, placeIn, readStrict, StrictJSONError } from "./json-document.ts";
|
|
3
4
|
import { dirnamePath, isAbsolutePath, joinPath, resolvePath } from "./paths.ts";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const ROLE_TARGET = "target";
|
|
8
|
-
const ROLE_CONSUMER = "consumer";
|
|
5
|
+
import { message } from "./product.ts";
|
|
6
|
+
import { firstRefusal } from "./schema.ts";
|
|
7
|
+
import { UsageError } from "./usage.ts";
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
10
|
* A scope document above this length is refused rather than decoded. It names one
|
|
@@ -14,8 +13,14 @@ const ROLE_CONSUMER = "consumer";
|
|
|
14
13
|
const MAX_DOCUMENT_LENGTH = 1 << 20;
|
|
15
14
|
|
|
16
15
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* The bytes read of a scope document. A UTF-16 unit decodes from at most three bytes,
|
|
17
|
+
* so a document past the bound is past it within these.
|
|
18
|
+
*/
|
|
19
|
+
const MAX_DOCUMENT_BYTES = 3 * (MAX_DOCUMENT_LENGTH + 1);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A scope that cannot be read as one. It ends the run with the failure code: the run
|
|
23
|
+
* could not learn what to analyze, so it produced no answer.
|
|
19
24
|
*/
|
|
20
25
|
export class ScopeError extends Error {
|
|
21
26
|
constructor(message: string) {
|
|
@@ -25,12 +30,8 @@ export class ScopeError extends Error {
|
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
/**
|
|
28
|
-
* One module the run reads: the target or a declared consumer.
|
|
29
|
-
*
|
|
30
|
-
* The role a document declares is not carried here, because the resolved scope
|
|
31
|
-
* holds exactly one target and a list of consumers, so a module's role is where it
|
|
32
|
-
* sits. What a document declares is read and refused where it contradicts that
|
|
33
|
-
* position, which is the only place the declaration can be wrong.
|
|
33
|
+
* One module the run reads: the target or a declared consumer. The role a document
|
|
34
|
+
* declares is not carried, because a module's role is where the scope holds it.
|
|
34
35
|
*/
|
|
35
36
|
export interface Module {
|
|
36
37
|
/** The package name. A document may state it; otherwise it is empty. */
|
|
@@ -45,139 +46,52 @@ export interface Scope {
|
|
|
45
46
|
readonly consumers: readonly Module[];
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
/** The closed key list of the scope document, compared as bytes. */
|
|
54
|
-
const SCOPE_ROOT: KeyNode = {
|
|
55
|
-
kind: "section",
|
|
56
|
-
members: {
|
|
57
|
-
target: MODULE,
|
|
58
|
-
workspace: { kind: "leaf" },
|
|
59
|
-
consumers: { kind: "list", members: MODULE.members ?? {} },
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
64
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* One string member, or undefined where the document omits it. A member the
|
|
69
|
-
* document writes is a string holding at least one character, so `null` and `""`
|
|
70
|
-
* are refused rather than read as absent.
|
|
71
|
-
*/
|
|
72
|
-
function stringMember(
|
|
73
|
-
parent: Record<string, unknown>,
|
|
74
|
-
name: string,
|
|
75
|
-
at: string,
|
|
76
|
-
): string | undefined {
|
|
77
|
-
if (!Object.hasOwn(parent, name)) {
|
|
78
|
-
return undefined;
|
|
79
|
-
}
|
|
80
|
-
const value = parent[name];
|
|
81
|
-
if (typeof value !== "string") {
|
|
82
|
-
throw new ScopeError(`scope: ${at === "" ? name : `${at}.${name}`} is not a string`);
|
|
83
|
-
}
|
|
84
|
-
if (value === "") {
|
|
85
|
-
throw new ScopeError(`scope: ${at === "" ? name : `${at}.${name}`} names nothing`);
|
|
86
|
-
}
|
|
87
|
-
return value;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function readModule(value: unknown, at: string, role: string, documentDir: string): Module {
|
|
91
|
-
if (!isRecord(value)) {
|
|
92
|
-
throw new ScopeError(`scope: ${at} is not an object`);
|
|
93
|
-
}
|
|
94
|
-
const declared = stringMember(value, "role", at);
|
|
95
|
-
if (declared !== undefined && declared !== role) {
|
|
96
|
-
throw new ScopeError(
|
|
97
|
-
`scope: ${at}.role is ${JSON.stringify(declared)}, want ${JSON.stringify(role)}`,
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
const path = stringMember(value, "path", at);
|
|
101
|
-
if (path === undefined) {
|
|
102
|
-
throw new ScopeError(`scope: ${at}.path names nothing`);
|
|
103
|
-
}
|
|
104
|
-
return {
|
|
105
|
-
id: stringMember(value, "id", at) ?? "",
|
|
106
|
-
path: isAbsolutePath(path) ? path : joinPath(documentDir, path),
|
|
107
|
-
};
|
|
49
|
+
interface DeclaredModule {
|
|
50
|
+
readonly id?: string;
|
|
51
|
+
readonly path: string;
|
|
108
52
|
}
|
|
109
53
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
* does not declare, compared as bytes, and a key one object writes twice, which a
|
|
113
|
-
* parse cannot report because it keeps the last value.
|
|
114
|
-
*/
|
|
115
|
-
function checkKeys(text: string, file: string): void {
|
|
116
|
-
try {
|
|
117
|
-
checkDocument(text, SCOPE_ROOT, `scope: ${file}`);
|
|
118
|
-
} catch (error: unknown) {
|
|
119
|
-
if (error instanceof ConfigError) {
|
|
120
|
-
// The configuration's refusal names the nearest configuration key, which a
|
|
121
|
-
// scope document does not have.
|
|
122
|
-
throw new ScopeError(
|
|
123
|
-
error.kind === "unimplemented-key"
|
|
124
|
-
? `scope: ${file}: ${error.key} is not a declared key`
|
|
125
|
-
: error.message,
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
throw error;
|
|
129
|
-
}
|
|
54
|
+
function refusal(file: string, place: string, reason: string): ScopeError {
|
|
55
|
+
return new ScopeError(`scope: ${file}: ${place === "" ? "" : `${place}: `}${reason}`);
|
|
130
56
|
}
|
|
131
57
|
|
|
132
58
|
/**
|
|
133
|
-
* Decodes the scope document at `file
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* resolves
|
|
137
|
-
*
|
|
138
|
-
* The `workspace` key the format declares is checked and carries nothing here: it
|
|
139
|
-
* names the file a Go consumer resolves the target through, and a TypeScript
|
|
140
|
-
* project resolves one through its own compiler configuration.
|
|
59
|
+
* Decodes the scope document at `file`, held to the Contract's scope schema, a relative
|
|
60
|
+
* path resolved against the document's own directory. A document that cannot be read is
|
|
61
|
+
* a usage error, one that is refused a scope error. `workspace` is read by Go consumers
|
|
62
|
+
* alone: a TypeScript project resolves the target through its own compiler configuration.
|
|
141
63
|
*/
|
|
142
64
|
export function readScope(host: Host, file: string): Scope {
|
|
143
65
|
const absolute = resolvePath(host.workingDirectory(), file);
|
|
144
66
|
let text: string;
|
|
145
67
|
try {
|
|
146
|
-
text = host.readFile(absolute);
|
|
68
|
+
text = host.readFile(absolute, MAX_DOCUMENT_BYTES);
|
|
147
69
|
} catch (error: unknown) {
|
|
148
|
-
throw new
|
|
149
|
-
`scope: ${file}: ${error instanceof Error ? error.message : String(error)}`,
|
|
150
|
-
);
|
|
70
|
+
throw new UsageError(`scope: ${file}: ${message(error)}`);
|
|
151
71
|
}
|
|
152
72
|
if (text.length > MAX_DOCUMENT_LENGTH) {
|
|
153
73
|
throw new ScopeError(`scope: ${file} is longer than ${String(MAX_DOCUMENT_LENGTH)} characters`);
|
|
154
74
|
}
|
|
155
75
|
let value: unknown;
|
|
156
76
|
try {
|
|
157
|
-
value =
|
|
77
|
+
value = readStrict(text).value;
|
|
158
78
|
} catch (error: unknown) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
79
|
+
if (!(error instanceof StrictJSONError)) {
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
throw refusal(file, dotted(error.place), locatedFault(text, error));
|
|
162
83
|
}
|
|
163
|
-
|
|
164
|
-
|
|
84
|
+
const refused = firstRefusal(SCOPE_SCHEMA, value);
|
|
85
|
+
if (refused !== undefined) {
|
|
86
|
+
throw refusal(file, dotted(placeIn(value, refused.pointer, refused.member)), refused.reason);
|
|
165
87
|
}
|
|
166
|
-
|
|
167
|
-
stringMember(value, "workspace", "");
|
|
88
|
+
const declared = value as { target: DeclaredModule; consumers?: readonly DeclaredModule[] };
|
|
168
89
|
const documentDir = dirnamePath(absolute);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (!Array.isArray(listed)) {
|
|
175
|
-
throw new ScopeError(`scope: ${file}: consumers is not an array`);
|
|
176
|
-
}
|
|
177
|
-
const consumers = (listed as unknown[]).map((entry, index) =>
|
|
178
|
-
readModule(entry, `consumers[${String(index)}]`, ROLE_CONSUMER, documentDir),
|
|
179
|
-
);
|
|
180
|
-
return { target, consumers };
|
|
90
|
+
const moduleOf = (module: DeclaredModule): Module => ({
|
|
91
|
+
id: module.id ?? "",
|
|
92
|
+
path: isAbsolutePath(module.path) ? module.path : joinPath(documentDir, module.path),
|
|
93
|
+
});
|
|
94
|
+
return { target: moduleOf(declared.target), consumers: (declared.consumers ?? []).map(moduleOf) };
|
|
181
95
|
}
|
|
182
96
|
|
|
183
97
|
/** The single-package scope one directory is, when no scope document is given. */
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
type SourceFile,
|
|
54
54
|
type SpreadAssignment,
|
|
55
55
|
} from "@typescript/native/ast";
|
|
56
|
+
import { parenthesesRemoved } from "./unwrap.ts";
|
|
56
57
|
import {
|
|
57
58
|
SignatureKind,
|
|
58
59
|
SymbolFlags,
|
|
@@ -81,7 +82,7 @@ import { erasedStores, type ErasedStores } from "./erased-stores.ts";
|
|
|
81
82
|
import { checkedTypes, CONVERSION_MEMBERS, valueReach, type Reached } from "./value-reach.ts";
|
|
82
83
|
|
|
83
84
|
/** The serializer every run reads, whatever the configuration names. */
|
|
84
|
-
const JSON_STRINGIFY: DeclarationEntry = {
|
|
85
|
+
const JSON_STRINGIFY: DeclarationEntry = { global: "JSON.stringify" };
|
|
85
86
|
|
|
86
87
|
/** The methods a serializer and a formatter resolve by name on a value that left the analysis. */
|
|
87
88
|
const CONVERSIONS: ReadonlySet<string> = new Set(["toJSON", "toString"]);
|
|
@@ -227,12 +228,6 @@ interface MethodForm {
|
|
|
227
228
|
readonly invocation: Invocation;
|
|
228
229
|
}
|
|
229
230
|
|
|
230
|
-
function unparenthesized(expression: Expression): Expression {
|
|
231
|
-
return isParenthesizedExpression(expression)
|
|
232
|
-
? unparenthesized(expression.expression)
|
|
233
|
-
: expression;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
231
|
/**
|
|
237
232
|
* The invocation a call writes through a method of `Function.prototype`, read from
|
|
238
233
|
* the syntax alone: `f.call(self, a)`, `f.apply(self, [a])`, `f.bind(self, a)`, which
|
|
@@ -241,12 +236,12 @@ function unparenthesized(expression: Expression): Expression {
|
|
|
241
236
|
* carries. Whether the method is the prototype's is resolution's to say.
|
|
242
237
|
*/
|
|
243
238
|
function throughMethod(call: CallExpression): MethodForm | undefined {
|
|
244
|
-
const expression =
|
|
239
|
+
const expression = parenthesesRemoved(call.expression);
|
|
245
240
|
if (isPropertyAccessExpression(expression) && FUNCTION_METHODS.has(expression.name.text)) {
|
|
246
241
|
const [, ...rest] = call.arguments;
|
|
247
242
|
let passed: readonly Expression[] = rest;
|
|
248
243
|
if (expression.name.text === "apply") {
|
|
249
|
-
const list = rest[0] === undefined ? undefined :
|
|
244
|
+
const list = rest[0] === undefined ? undefined : parenthesesRemoved(rest[0]);
|
|
250
245
|
passed = list === undefined ? [] : isArrayLiteralExpression(list) ? list.elements : [list];
|
|
251
246
|
}
|
|
252
247
|
return {
|
|
@@ -255,7 +250,7 @@ function throughMethod(call: CallExpression): MethodForm | undefined {
|
|
|
255
250
|
};
|
|
256
251
|
}
|
|
257
252
|
if (isCallExpression(expression)) {
|
|
258
|
-
const bound =
|
|
253
|
+
const bound = parenthesesRemoved(expression.expression);
|
|
259
254
|
if (isPropertyAccessExpression(bound) && bound.name.text === "bind") {
|
|
260
255
|
return {
|
|
261
256
|
method: bound.name,
|
package/src/session.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type Queries,
|
|
27
27
|
type UnansweredQuestion,
|
|
28
28
|
} from "./query.ts";
|
|
29
|
+
import { MANIFEST } from "./fs/manifest.ts";
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* The compiler the analysis reads through: a client over a protocol to a separate
|
|
@@ -115,7 +116,7 @@ export function openEngine(options: EngineOptions): Engine {
|
|
|
115
116
|
if (components !== undefined && components.extensions.length > 0) {
|
|
116
117
|
const made = components.host.temporaryDirectory();
|
|
117
118
|
try {
|
|
118
|
-
components.host.writeDocument(joinPath(made.path,
|
|
119
|
+
components.host.writeDocument(joinPath(made.path, MANIFEST), [
|
|
119
120
|
mapperManifest(components.host.componentMapperCommand()),
|
|
120
121
|
]);
|
|
121
122
|
} catch (error: unknown) {
|
package/src/suppress-file.ts
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
import { CODE } from "./contract/contract.generated.ts";
|
|
9
9
|
import type { Host } from "./host.ts";
|
|
10
10
|
import type { InventorySymbol } from "./inventory.ts";
|
|
11
|
+
import { readTargetDocument } from "./fs/document.ts";
|
|
11
12
|
import { jsonChunks } from "./json-chunks.ts";
|
|
12
|
-
import {
|
|
13
|
-
import { positionAt, walkDocument } from "./json-document.ts";
|
|
13
|
+
import { isRecord, positionAt, readStrict, StrictJSONError } from "./json-document.ts";
|
|
14
14
|
import type { Position } from "./position.ts";
|
|
15
15
|
import { declarationKey } from "./ref.ts";
|
|
16
16
|
import {
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type SuppressionRecord,
|
|
27
27
|
type Suppressions,
|
|
28
28
|
} from "./suppress.ts";
|
|
29
|
+
import { message } from "./product.ts";
|
|
29
30
|
|
|
30
31
|
/** The name and the location the grammar fixes for the ignore file. */
|
|
31
32
|
export const IGNORE_FILE = "deadset-ignore.json";
|
|
@@ -79,10 +80,6 @@ function documentSite(shape: Shape): Position {
|
|
|
79
80
|
return { path: shape.file, line: 0, column: 0 };
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
function isObject(value: unknown): value is Record<string, unknown> {
|
|
83
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
83
|
/** What one entry or row resolves to. */
|
|
87
84
|
interface Read {
|
|
88
85
|
readonly records: readonly SuppressionRecord[];
|
|
@@ -100,7 +97,7 @@ function readRecord(value: unknown, site: Position, byReference: ByReference, sh
|
|
|
100
97
|
const refuse = (text: string, want: string): never => {
|
|
101
98
|
throw new SuppressionError(shape.mechanism, siteText(site), text, want);
|
|
102
99
|
};
|
|
103
|
-
if (!
|
|
100
|
+
if (!isRecord(value)) {
|
|
104
101
|
return refuse(JSON.stringify(value), shape.recordWant);
|
|
105
102
|
}
|
|
106
103
|
for (const [key, held] of Object.entries(value)) {
|
|
@@ -188,30 +185,26 @@ function readDocument(
|
|
|
188
185
|
symbols: readonly InventorySymbol[],
|
|
189
186
|
shape: Shape,
|
|
190
187
|
): Suppressions {
|
|
191
|
-
const
|
|
192
|
-
if (
|
|
188
|
+
const text = readTargetDocument(host, targetRoot, shape.file);
|
|
189
|
+
if (text === undefined) {
|
|
193
190
|
return { records: [], refusals: [] };
|
|
194
191
|
}
|
|
195
|
-
const text = host.readFile(file);
|
|
196
192
|
const malformed = (detail: string, site = documentSite(shape)): never => {
|
|
197
193
|
throw new SuppressionError(shape.mechanism, siteText(site), detail, shape.documentWant);
|
|
198
194
|
};
|
|
199
|
-
let
|
|
195
|
+
let document;
|
|
200
196
|
try {
|
|
201
|
-
|
|
197
|
+
document = readStrict(text, shape.array);
|
|
202
198
|
} catch (error: unknown) {
|
|
203
|
-
|
|
199
|
+
if (!(error instanceof StrictJSONError)) {
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
return malformed(error.message, positionAt(text, shape.file, error.offset));
|
|
204
203
|
}
|
|
205
|
-
|
|
204
|
+
const parsed = document.value;
|
|
205
|
+
if (!isRecord(parsed)) {
|
|
206
206
|
return malformed(shape.file);
|
|
207
207
|
}
|
|
208
|
-
const walk = walkDocument(text, shape.array);
|
|
209
|
-
if (walk.twice !== undefined) {
|
|
210
|
-
malformed(
|
|
211
|
-
`the member ${JSON.stringify(walk.twice.name)} is written twice, so neither value is chosen`,
|
|
212
|
-
positionAt(text, shape.file, walk.twice.offset),
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
208
|
for (const key of Object.keys(parsed)) {
|
|
216
209
|
if (key !== "description" && key !== shape.array) {
|
|
217
210
|
malformed(`the member ${JSON.stringify(key)} is not one the grammar declares`);
|
|
@@ -227,7 +220,7 @@ function readDocument(
|
|
|
227
220
|
const records: SuppressionRecord[] = [];
|
|
228
221
|
const refusals: Refusal[] = [];
|
|
229
222
|
(held as readonly unknown[]).forEach((value, index) => {
|
|
230
|
-
const offset =
|
|
223
|
+
const offset = document.starts.get(index);
|
|
231
224
|
const site = offset === undefined ? documentSite(shape) : positionAt(text, shape.file, offset);
|
|
232
225
|
const read = readRecord(value, site, byReference, shape);
|
|
233
226
|
records.push(...read.records);
|
|
@@ -328,9 +321,9 @@ export function otherSchemeRows(text: string, own: string = OWN_SCHEME): readonl
|
|
|
328
321
|
try {
|
|
329
322
|
parsed = JSON.parse(text);
|
|
330
323
|
} catch (error: unknown) {
|
|
331
|
-
return refuse(
|
|
324
|
+
return refuse(message(error));
|
|
332
325
|
}
|
|
333
|
-
const held =
|
|
326
|
+
const held = isRecord(parsed) ? parsed[BASELINE_SHAPE.array] : undefined;
|
|
334
327
|
if (!Array.isArray(held)) {
|
|
335
328
|
return refuse(BASELINE_SHAPE.documentWant);
|
|
336
329
|
}
|
|
@@ -338,7 +331,7 @@ export function otherSchemeRows(text: string, own: string = OWN_SCHEME): readonl
|
|
|
338
331
|
try {
|
|
339
332
|
readRecord(value, documentSite(BASELINE_SHAPE), new Map(), BASELINE_SHAPE);
|
|
340
333
|
} catch (error: unknown) {
|
|
341
|
-
return refuse(
|
|
334
|
+
return refuse(message(error));
|
|
342
335
|
}
|
|
343
336
|
// readRecord has checked every member is a string and the code and symbol are present.
|
|
344
337
|
const { code, symbol, path, reason } = value as Readonly<Record<string, string>>;
|
|
@@ -385,10 +378,9 @@ function* separatorsEscaped(pieces: Iterable<string>): Generator<string, void, u
|
|
|
385
378
|
* A baseline holding one row per finding, in the order given, which is the report's, and
|
|
386
379
|
* the other language's rows `kept`, Go's rows ahead of TypeScript's and each scheme's in
|
|
387
380
|
* the order given, so two analyzers writing one document in either order write the same
|
|
388
|
-
* bytes, in the form grammar/suppression.md fixes. Every
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
* rather than written, because the grammar requires a reason on every row.
|
|
381
|
+
* bytes, in the form grammar/suppression.md fixes. Every row carries the provenance as
|
|
382
|
+
* its reason, so a finding missing a value a row needs, or an identity missing its name
|
|
383
|
+
* or version, is refused rather than written.
|
|
392
384
|
*/
|
|
393
385
|
export function writeBaseline(
|
|
394
386
|
findings: readonly Recorded[],
|
package/src/suppress.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
SYMBOL_SUBJECTS,
|
|
14
14
|
type Subject,
|
|
15
15
|
} from "./contract/contract.generated.ts";
|
|
16
|
-
import {
|
|
16
|
+
import { UsageError } from "./usage.ts";
|
|
17
17
|
import type { Finding } from "./finding.ts";
|
|
18
18
|
import { positionKey, type Position } from "./position.ts";
|
|
19
19
|
import { isRef } from "./ref.ts";
|
|
@@ -68,9 +68,9 @@ export interface Suppressions {
|
|
|
68
68
|
* instruction the analysis cannot carry out. It is a malformed input, so the command line
|
|
69
69
|
* answers it with the usage code.
|
|
70
70
|
*/
|
|
71
|
-
export class SuppressionError extends
|
|
71
|
+
export class SuppressionError extends UsageError {
|
|
72
72
|
constructor(mechanism: Mechanism, at: string, text: string, want: string) {
|
|
73
|
-
super(
|
|
73
|
+
super(`${mechanism} ${at}: ${text}: want ${want}`);
|
|
74
74
|
this.name = "SuppressionError";
|
|
75
75
|
}
|
|
76
76
|
}
|
package/src/symbol-keys.ts
CHANGED
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
isIdentifier,
|
|
13
13
|
isNoSubstitutionTemplateLiteral,
|
|
14
14
|
isNumericLiteral,
|
|
15
|
-
isParenthesizedExpression,
|
|
16
15
|
isPropertyAccessExpression,
|
|
17
16
|
isSpreadElement,
|
|
18
17
|
isStringLiteral,
|
|
@@ -21,6 +20,7 @@ import {
|
|
|
21
20
|
type Node,
|
|
22
21
|
type SourceFile,
|
|
23
22
|
} from "@typescript/native/ast";
|
|
23
|
+
import { parenthesesRemoved } from "./unwrap.ts";
|
|
24
24
|
import type { Symbol as TSSymbol, Type } from "@typescript/native/sync";
|
|
25
25
|
import type { AliasChains } from "./alias-chain.ts";
|
|
26
26
|
import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
|
|
@@ -52,10 +52,7 @@ interface KeyedUse {
|
|
|
52
52
|
|
|
53
53
|
/** The identifier an element access's argument names its key by, where it is a name. */
|
|
54
54
|
export function keyNameOf(argument: Node): Node | undefined {
|
|
55
|
-
|
|
56
|
-
while (isParenthesizedExpression(at)) {
|
|
57
|
-
at = at.expression;
|
|
58
|
-
}
|
|
55
|
+
const at = parenthesesRemoved(argument);
|
|
59
56
|
if (isIdentifier(at)) {
|
|
60
57
|
return at;
|
|
61
58
|
}
|
package/src/template-field.ts
CHANGED
|
@@ -13,13 +13,16 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { readComponent } from "./component-files.ts";
|
|
16
|
-
import {
|
|
16
|
+
import type { ResolvedConfig, TemplateDelimiters } from "./config.ts";
|
|
17
17
|
import type { DetectorInput, Evidence } from "./exempt.ts";
|
|
18
18
|
import type { Host } from "./host.ts";
|
|
19
19
|
import type { SymbolKind } from "./inventory.ts";
|
|
20
20
|
import { memberNames } from "./member-names.ts";
|
|
21
21
|
import { extensionOf, isAbsolutePath, joinPath, normalizePath, relativePath } from "./paths.ts";
|
|
22
22
|
import { isComponentFile, type Position } from "./position.ts";
|
|
23
|
+
import { bytewise } from "./text/compare.ts";
|
|
24
|
+
import { filesBelow } from "./fs/tree.ts";
|
|
25
|
+
import { UsageError } from "./usage.ts";
|
|
23
26
|
|
|
24
27
|
/** One template, by its path below the target root. */
|
|
25
28
|
interface TemplateFile {
|
|
@@ -50,14 +53,6 @@ const READ_BY_MARKUP: ReadonlySet<SymbolKind> = new Set([...READ_BY_A_TEMPLATE,
|
|
|
50
53
|
/** One identifier, as the grammar of the analyzed language spells one. */
|
|
51
54
|
const IDENTIFIER = /[\p{ID_Start}$_][\p{ID_Continue}$\u200C\u200D]*/uy;
|
|
52
55
|
|
|
53
|
-
/** Two strings ordered bytewise. */
|
|
54
|
-
function compare(a: string, b: string): number {
|
|
55
|
-
if (a === b) {
|
|
56
|
-
return 0;
|
|
57
|
-
}
|
|
58
|
-
return a < b ? -1 : 1;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
56
|
/**
|
|
62
57
|
* Every template under the configured directories, read once for the run, in path
|
|
63
58
|
* order and each file once. A directory is named relative to the target root, and
|
|
@@ -66,25 +61,22 @@ function compare(a: string, b: string): number {
|
|
|
66
61
|
* exemption it was configured for off. A symbolic link to a directory is not
|
|
67
62
|
* followed.
|
|
68
63
|
*/
|
|
69
|
-
export function readTemplates(
|
|
64
|
+
export function readTemplates(
|
|
65
|
+
host: Host,
|
|
66
|
+
targetRoot: string,
|
|
67
|
+
analysis: ResolvedConfig["analysis"],
|
|
68
|
+
): Templates {
|
|
70
69
|
const files = new Map<string, string>();
|
|
71
70
|
const read = (dir: string): void => {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
);
|
|
75
|
-
for (const entry of entries) {
|
|
76
|
-
const path = joinPath(dir, entry.name);
|
|
77
|
-
if (entry.directory) {
|
|
78
|
-
read(path);
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
71
|
+
for (const below of filesBelow(host, joinPath(targetRoot, dir), () => true)) {
|
|
72
|
+
const path = joinPath(dir, below);
|
|
81
73
|
const absolute = joinPath(targetRoot, path);
|
|
82
74
|
if (!files.has(path) && host.kindOf(absolute) === "file") {
|
|
83
75
|
files.set(path, host.readFile(absolute));
|
|
84
76
|
}
|
|
85
77
|
}
|
|
86
78
|
};
|
|
87
|
-
for (const configured of analysis.
|
|
79
|
+
for (const configured of analysis.template_dirs) {
|
|
88
80
|
const dir = normalizePath(configured);
|
|
89
81
|
if (
|
|
90
82
|
isAbsolutePath(dir) ||
|
|
@@ -92,19 +84,17 @@ export function readTemplates(host: Host, targetRoot: string, analysis: Analysis
|
|
|
92
84
|
dir.startsWith("../") ||
|
|
93
85
|
host.kindOf(joinPath(targetRoot, dir)) !== "directory"
|
|
94
86
|
) {
|
|
95
|
-
throw new
|
|
96
|
-
"malformed",
|
|
97
|
-
"analysis.template_dirs",
|
|
87
|
+
throw new UsageError(
|
|
98
88
|
`analysis.template_dirs: ${JSON.stringify(configured)} is not a directory below the target root`,
|
|
99
89
|
);
|
|
100
90
|
}
|
|
101
91
|
read(dir);
|
|
102
92
|
}
|
|
103
93
|
return {
|
|
104
|
-
delimiters: analysis.
|
|
94
|
+
delimiters: analysis.template_delimiters,
|
|
105
95
|
files: [...files]
|
|
106
96
|
.map(([path, text]) => ({ path, text }))
|
|
107
|
-
.sort((a, b) =>
|
|
97
|
+
.sort((a, b) => bytewise(a.path, b.path)),
|
|
108
98
|
};
|
|
109
99
|
}
|
|
110
100
|
|
package/src/test-globals.ts
CHANGED
|
@@ -25,10 +25,12 @@ import {
|
|
|
25
25
|
} from "@typescript/native/ast";
|
|
26
26
|
import type { ParseFile } from "./configuration-files.ts";
|
|
27
27
|
import type { Host } from "./host.ts";
|
|
28
|
-
import { dirnamePath, joinPath } from "./paths.ts";
|
|
28
|
+
import { ancestors, dirnamePath, joinPath } from "./paths.ts";
|
|
29
29
|
import { UNANSWERED } from "./query.ts";
|
|
30
30
|
import type { Engine, ProjectView } from "./session.ts";
|
|
31
31
|
import { writtenPath, type WorkspaceProgram } from "./workspace-programs.ts";
|
|
32
|
+
import { filesIn } from "./fs/tree.ts";
|
|
33
|
+
import { holdsManifest, INSTALLED_PACKAGES } from "./fs/manifest.ts";
|
|
32
34
|
|
|
33
35
|
/** The test runner whose configuration injects the globals, and the `types` entry declaring them. */
|
|
34
36
|
export const GLOBALS_RUNNER = "vitest";
|
|
@@ -120,16 +122,7 @@ function setsGlobals(file: SourceFile): boolean {
|
|
|
120
122
|
|
|
121
123
|
/** Whether the runner's configuration in one manifest's directory injects the globals. */
|
|
122
124
|
function injectsGlobals(host: Host, parse: ParseFile, dir: string): boolean {
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
names = host
|
|
126
|
-
.readDirectory(dir)
|
|
127
|
-
.filter((entry) => !entry.directory && RUNNER_CONFIG.test(entry.name))
|
|
128
|
-
.map((entry) => entry.name);
|
|
129
|
-
} catch {
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
return names.some((name) => {
|
|
125
|
+
return filesIn(host, dir, (name) => RUNNER_CONFIG.test(name)).some((name) => {
|
|
133
126
|
const path = joinPath(dir, name);
|
|
134
127
|
try {
|
|
135
128
|
return setsGlobals(parse(path, host.readFile(path)));
|
|
@@ -141,26 +134,15 @@ function injectsGlobals(host: Host, parse: ParseFile, dir: string): boolean {
|
|
|
141
134
|
|
|
142
135
|
/** Whether a file declaring the globals is installed where the configuration resolves packages from. */
|
|
143
136
|
function globalsInstalled(host: Host, configFile: string): boolean {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (dirnamePath(dir) === dir) {
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
137
|
+
return ancestors(dirnamePath(configFile)).some(
|
|
138
|
+
(dir) =>
|
|
139
|
+
host.kindOf(joinPath(dir, INSTALLED_PACKAGES, GLOBALS_RUNNER, "globals.d.ts")) === "file",
|
|
140
|
+
);
|
|
152
141
|
}
|
|
153
142
|
|
|
154
143
|
/** The directory of the manifest nearest one configuration file, at or above its own. */
|
|
155
144
|
function manifestDir(host: Host, configFile: string): string | undefined {
|
|
156
|
-
|
|
157
|
-
if (host.kindOf(joinPath(dir, "package.json")) === "file") {
|
|
158
|
-
return dir;
|
|
159
|
-
}
|
|
160
|
-
if (dirnamePath(dir) === dir) {
|
|
161
|
-
return undefined;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
145
|
+
return ancestors(dirnamePath(configFile)).find((dir) => holdsManifest(host, dir));
|
|
164
146
|
}
|
|
165
147
|
|
|
166
148
|
function typesOf(value: unknown): readonly string[] | undefined {
|
package/src/test-joins.ts
CHANGED
|
@@ -8,9 +8,11 @@
|
|
|
8
8
|
|
|
9
9
|
import { sourcePaths } from "./file-facts.ts";
|
|
10
10
|
import type { Host } from "./host.ts";
|
|
11
|
-
import { dirnamePath, joinPath
|
|
11
|
+
import { ancestors, dirnamePath, joinPath } from "./paths.ts";
|
|
12
12
|
import type { Engine } from "./session.ts";
|
|
13
13
|
import { writtenPath, type WorkspaceProgram } from "./workspace-programs.ts";
|
|
14
|
+
import { bytewise } from "./text/compare.ts";
|
|
15
|
+
import { holdsManifest } from "./fs/manifest.ts";
|
|
14
16
|
|
|
15
17
|
const PREFERRED = "/tsconfig.json";
|
|
16
18
|
|
|
@@ -18,7 +20,7 @@ const PREFERRED = "/tsconfig.json";
|
|
|
18
20
|
function byPreference(a: string, b: string): number {
|
|
19
21
|
const first = a.endsWith(PREFERRED) ? 0 : 1;
|
|
20
22
|
const second = b.endsWith(PREFERRED) ? 0 : 1;
|
|
21
|
-
return first - second || (a
|
|
23
|
+
return first - second || bytewise(a, b);
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
/**
|
|
@@ -54,17 +56,13 @@ export function joinedTestFiles(
|
|
|
54
56
|
if (listed.has(file) || !patterns.some((pattern) => pattern.test(path))) {
|
|
55
57
|
continue;
|
|
56
58
|
}
|
|
57
|
-
for (
|
|
59
|
+
for (const dir of ancestors(dirnamePath(file), root)) {
|
|
58
60
|
const [nearest] = byDir.get(dir) ?? [];
|
|
59
61
|
if (nearest !== undefined) {
|
|
60
62
|
joins.set(nearest, [...(joins.get(nearest) ?? []), file]);
|
|
61
63
|
break;
|
|
62
64
|
}
|
|
63
|
-
if (
|
|
64
|
-
host.kindOf(joinPath(dir, "package.json")) === "file" ||
|
|
65
|
-
dir === root ||
|
|
66
|
-
relativePath(root, dir) === undefined
|
|
67
|
-
) {
|
|
65
|
+
if (holdsManifest(host, dir)) {
|
|
68
66
|
break;
|
|
69
67
|
}
|
|
70
68
|
}
|