@cplieger/deadset-ts 8.0.0-dev.7 → 8.0.0-dev.9
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/node-host.js +21 -3
- package/dist/src/analysis.js +94 -299
- package/dist/src/config.js +6 -83
- package/dist/src/configuration-files.js +2 -9
- package/dist/src/configured-declarations.js +15 -20
- package/dist/src/consumers.js +3 -30
- package/dist/src/contract/config.generated.js +544 -0
- package/dist/src/contract/contract.generated.js +20 -10
- package/dist/src/contract/report.generated.js +3 -2
- package/dist/src/conventions.js +3 -8
- package/dist/src/edges.js +36 -112
- package/dist/src/exempt-classes.js +0 -2
- package/dist/src/exempt.js +1 -1
- package/dist/src/failure.js +36 -0
- 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/unused-declarations.js +1 -1
- package/dist/src/findings/visibility-narrowing.js +2 -2
- package/dist/src/findings-pass.js +1 -1
- package/dist/src/framework-lifecycle.js +7 -7
- package/dist/src/fs/document.js +52 -0
- package/dist/src/fs/manifest.js +7 -0
- package/dist/src/injection-container.js +1 -1
- package/dist/src/json-document.js +194 -67
- package/dist/src/kinds.js +0 -7
- package/dist/src/load/classify.js +236 -0
- package/dist/src/load/diagnostics.js +79 -0
- package/dist/src/load/discover.js +230 -0
- package/dist/src/{emit-map.js → load/emit-map.js} +1 -1
- package/dist/src/load/expected-module.js +136 -0
- package/dist/src/load/load.js +22 -0
- package/dist/src/load/module-lookup.js +37 -0
- package/dist/src/load/plan.js +155 -0
- package/dist/src/{test-globals.js → load/test-globals.js} +51 -56
- package/dist/src/load/test-joins.js +82 -0
- package/dist/src/{workspace-manifests.js → load/workspace-manifests.js} +3 -3
- package/dist/src/{workspace-programs.js → load/workspace-programs.js} +37 -40
- package/dist/src/{workspace-resolution.js → load/workspace-resolution.js} +21 -59
- package/dist/src/{workspace-run.js → load/workspace-run.js} +4 -3
- package/dist/src/{workspace.js → load/workspace.js} +21 -17
- package/dist/src/outside-tests.js +1 -1
- package/dist/src/print.js +10 -79
- package/dist/src/report-schema.js +3 -224
- package/dist/src/resolve.js +245 -715
- package/dist/src/run.js +59 -72
- package/dist/src/schema.js +320 -344
- package/dist/src/scope.js +37 -113
- package/dist/src/serialization-contract.js +1 -1
- package/dist/src/session.js +22 -28
- package/dist/src/source-files.js +1 -1
- package/dist/src/suppress-file.js +15 -17
- package/dist/src/suppress.js +3 -3
- package/dist/src/template-field.js +4 -4
- package/dist/src/type-errors.js +11 -216
- package/dist/src/usage.js +10 -0
- package/dist/src/verbs/analyze.js +100 -122
- package/dist/src/verbs/describe.js +5 -12
- package/dist/src/verbs/explain.js +3 -3
- package/dist/src/verbs/print-projects.js +2 -2
- package/dist/src/verbs/version.js +12 -2
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +95 -460
- package/src/config.ts +31 -298
- package/src/configuration-files.ts +8 -10
- package/src/configured-declarations.ts +18 -23
- package/src/consumers.ts +6 -57
- package/src/contract/config.generated.ts +798 -0
- package/src/contract/contract.generated.ts +20 -10
- package/src/contract/report.generated.ts +3 -2
- package/src/conventions.ts +6 -12
- package/src/edges.ts +39 -124
- package/src/exempt-classes.ts +0 -5
- package/src/exempt.ts +4 -4
- package/src/failure.ts +63 -0
- 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/unused-declarations.ts +1 -1
- package/src/findings/visibility-narrowing.ts +2 -2
- package/src/findings-pass.ts +3 -3
- package/src/framework-lifecycle.ts +7 -7
- package/src/fs/document.ts +70 -0
- package/src/fs/manifest.ts +8 -0
- package/src/host.ts +12 -5
- package/src/injection-container.ts +1 -1
- package/src/json-document.ts +236 -76
- package/src/kinds.ts +0 -8
- package/src/load/classify.ts +352 -0
- package/src/load/diagnostics.ts +141 -0
- package/src/load/discover.ts +378 -0
- package/src/{emit-map.ts → load/emit-map.ts} +1 -1
- package/src/load/expected-module.ts +188 -0
- package/src/load/load.ts +61 -0
- package/src/load/module-lookup.ts +50 -0
- package/src/load/plan.ts +258 -0
- package/src/{test-globals.ts → load/test-globals.ts} +59 -65
- package/src/load/test-joins.ts +98 -0
- package/src/{workspace-manifests.ts → load/workspace-manifests.ts} +4 -4
- package/src/{workspace-programs.ts → load/workspace-programs.ts} +59 -53
- package/src/{workspace-resolution.ts → load/workspace-resolution.ts} +31 -78
- package/src/{workspace-run.ts → load/workspace-run.ts} +14 -11
- package/src/{workspace.ts → load/workspace.ts} +28 -21
- package/src/outside-tests.ts +1 -1
- package/src/partial.ts +1 -1
- package/src/print.ts +11 -92
- package/src/report-schema.ts +3 -299
- package/src/resolve.ts +281 -1105
- package/src/run.ts +68 -80
- package/src/schema.ts +391 -389
- package/src/scope.ts +45 -131
- package/src/serialization-contract.ts +1 -1
- package/src/session.ts +52 -53
- package/src/source-files.ts +1 -1
- package/src/suppress-file.ts +15 -20
- package/src/suppress.ts +3 -3
- package/src/template-field.ts +10 -7
- package/src/type-errors.ts +18 -288
- package/src/usage.ts +10 -0
- package/src/verbs/analyze.ts +139 -157
- package/src/verbs/describe.ts +4 -11
- package/src/verbs/explain.ts +3 -7
- package/src/verbs/print-projects.ts +2 -2
- package/src/verbs/verb.ts +2 -2
- package/src/verbs/version.ts +13 -2
- package/dist/src/base-url.js +0 -18
- package/dist/src/discover.js +0 -374
- package/dist/src/setup-failure.js +0 -19
- package/dist/src/test-joins.js +0 -101
- package/src/base-url.ts +0 -24
- package/src/discover.ts +0 -548
- package/src/setup-failure.ts +0 -37
- package/src/test-joins.ts +0 -118
package/src/scope.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
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
|
-
import {
|
|
5
|
+
import { MARKERS } from "./load/discover.ts";
|
|
6
|
+
import { failureExit } from "./failure.ts";
|
|
6
7
|
import { message } from "./product.ts";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const ROLE_TARGET = "target";
|
|
10
|
-
const ROLE_CONSUMER = "consumer";
|
|
8
|
+
import { firstRefusal } from "./schema.ts";
|
|
9
|
+
import { UsageError } from "./usage.ts";
|
|
11
10
|
|
|
12
11
|
/**
|
|
13
12
|
* A scope document above this length is refused rather than decoded. It names one
|
|
@@ -16,23 +15,14 @@ const ROLE_CONSUMER = "consumer";
|
|
|
16
15
|
const MAX_DOCUMENT_LENGTH = 1 << 20;
|
|
17
16
|
|
|
18
17
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* The bytes read of a scope document. A UTF-16 unit decodes from at most three bytes,
|
|
19
|
+
* so a document past the bound is past it within these.
|
|
21
20
|
*/
|
|
22
|
-
|
|
23
|
-
constructor(message: string) {
|
|
24
|
-
super(message);
|
|
25
|
-
this.name = "ScopeError";
|
|
26
|
-
}
|
|
27
|
-
}
|
|
21
|
+
const MAX_DOCUMENT_BYTES = 3 * (MAX_DOCUMENT_LENGTH + 1);
|
|
28
22
|
|
|
29
23
|
/**
|
|
30
|
-
* One module the run reads: the target or a declared consumer.
|
|
31
|
-
*
|
|
32
|
-
* The role a document declares is not carried here, because the resolved scope
|
|
33
|
-
* holds exactly one target and a list of consumers, so a module's role is where it
|
|
34
|
-
* sits. What a document declares is read and refused where it contradicts that
|
|
35
|
-
* position, which is the only place the declaration can be wrong.
|
|
24
|
+
* One module the run reads: the target or a declared consumer. The role a document
|
|
25
|
+
* declares is not carried, because a module's role is where the scope holds it.
|
|
36
26
|
*/
|
|
37
27
|
export interface Module {
|
|
38
28
|
/** The package name. A document may state it; otherwise it is empty. */
|
|
@@ -47,142 +37,66 @@ export interface Scope {
|
|
|
47
37
|
readonly consumers: readonly Module[];
|
|
48
38
|
}
|
|
49
39
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
/** The closed key list of the scope document, compared as bytes. */
|
|
56
|
-
const SCOPE_ROOT: KeyNode = {
|
|
57
|
-
kind: "section",
|
|
58
|
-
members: {
|
|
59
|
-
target: MODULE,
|
|
60
|
-
workspace: { kind: "leaf" },
|
|
61
|
-
consumers: { kind: "list", members: MODULE.members ?? {} },
|
|
62
|
-
},
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* One string member, or undefined where the document omits it. A member the
|
|
67
|
-
* document writes is a string holding at least one character, so `null` and `""`
|
|
68
|
-
* are refused rather than read as absent.
|
|
69
|
-
*/
|
|
70
|
-
function stringMember(
|
|
71
|
-
parent: Record<string, unknown>,
|
|
72
|
-
name: string,
|
|
73
|
-
at: string,
|
|
74
|
-
): string | undefined {
|
|
75
|
-
if (!Object.hasOwn(parent, name)) {
|
|
76
|
-
return undefined;
|
|
77
|
-
}
|
|
78
|
-
const value = parent[name];
|
|
79
|
-
if (typeof value !== "string") {
|
|
80
|
-
throw new ScopeError(`scope: ${at === "" ? name : `${at}.${name}`} is not a string`);
|
|
81
|
-
}
|
|
82
|
-
if (value === "") {
|
|
83
|
-
throw new ScopeError(`scope: ${at === "" ? name : `${at}.${name}`} names nothing`);
|
|
84
|
-
}
|
|
85
|
-
return value;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function readModule(value: unknown, at: string, role: string, documentDir: string): Module {
|
|
89
|
-
if (!isRecord(value)) {
|
|
90
|
-
throw new ScopeError(`scope: ${at} is not an object`);
|
|
91
|
-
}
|
|
92
|
-
const declared = stringMember(value, "role", at);
|
|
93
|
-
if (declared !== undefined && declared !== role) {
|
|
94
|
-
throw new ScopeError(
|
|
95
|
-
`scope: ${at}.role is ${JSON.stringify(declared)}, want ${JSON.stringify(role)}`,
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
const path = stringMember(value, "path", at);
|
|
99
|
-
if (path === undefined) {
|
|
100
|
-
throw new ScopeError(`scope: ${at}.path names nothing`);
|
|
101
|
-
}
|
|
102
|
-
return {
|
|
103
|
-
id: stringMember(value, "id", at) ?? "",
|
|
104
|
-
path: isAbsolutePath(path) ? path : joinPath(documentDir, path),
|
|
105
|
-
};
|
|
40
|
+
interface DeclaredModule {
|
|
41
|
+
readonly id?: string;
|
|
42
|
+
readonly path: string;
|
|
106
43
|
}
|
|
107
44
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
* does not declare, compared as bytes, and a key one object writes twice, which a
|
|
111
|
-
* parse cannot report because it keeps the last value.
|
|
112
|
-
*/
|
|
113
|
-
function checkKeys(text: string, file: string): void {
|
|
114
|
-
try {
|
|
115
|
-
checkDocument(text, SCOPE_ROOT, `scope: ${file}`);
|
|
116
|
-
} catch (error: unknown) {
|
|
117
|
-
if (error instanceof ConfigError) {
|
|
118
|
-
// The configuration's refusal names the nearest configuration key, which a
|
|
119
|
-
// scope document does not have.
|
|
120
|
-
throw new ScopeError(
|
|
121
|
-
error.kind === "unimplemented-key"
|
|
122
|
-
? `scope: ${file}: ${error.key} is not a declared key`
|
|
123
|
-
: error.message,
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
throw error;
|
|
127
|
-
}
|
|
45
|
+
function refusal(file: string, place: string, reason: string): UsageError {
|
|
46
|
+
return new UsageError(`scope: ${file}: ${place === "" ? "" : `${place}: `}${reason}`);
|
|
128
47
|
}
|
|
129
48
|
|
|
130
49
|
/**
|
|
131
|
-
* Decodes the scope document at `file
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* resolves
|
|
135
|
-
*
|
|
136
|
-
* The `workspace` key the format declares is checked and carries nothing here: it
|
|
137
|
-
* names the file a Go consumer resolves the target through, and a TypeScript
|
|
138
|
-
* project resolves one through its own compiler configuration.
|
|
50
|
+
* Decodes the scope document at `file`, held to the Contract's scope schema, a relative
|
|
51
|
+
* path resolved against the document's own directory. A document that cannot be read or is
|
|
52
|
+
* refused is a usage error, as any document the invocation names. `workspace` is read by Go consumers
|
|
53
|
+
* alone: a TypeScript project resolves the target through its own compiler configuration.
|
|
139
54
|
*/
|
|
140
55
|
export function readScope(host: Host, file: string): Scope {
|
|
141
56
|
const absolute = resolvePath(host.workingDirectory(), file);
|
|
142
57
|
let text: string;
|
|
143
58
|
try {
|
|
144
|
-
text = host.readFile(absolute);
|
|
59
|
+
text = host.readFile(absolute, MAX_DOCUMENT_BYTES);
|
|
145
60
|
} catch (error: unknown) {
|
|
146
|
-
throw new
|
|
61
|
+
throw new UsageError(`scope: ${file}: ${message(error)}`);
|
|
147
62
|
}
|
|
148
63
|
if (text.length > MAX_DOCUMENT_LENGTH) {
|
|
149
|
-
throw new
|
|
64
|
+
throw new UsageError(`scope: ${file} is longer than ${String(MAX_DOCUMENT_LENGTH)} characters`);
|
|
150
65
|
}
|
|
151
66
|
let value: unknown;
|
|
152
67
|
try {
|
|
153
|
-
value =
|
|
68
|
+
value = readStrict(text).value;
|
|
154
69
|
} catch (error: unknown) {
|
|
155
|
-
|
|
70
|
+
if (!(error instanceof StrictJSONError)) {
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
throw refusal(file, dotted(error.place), locatedFault(text, error));
|
|
156
74
|
}
|
|
157
|
-
|
|
158
|
-
|
|
75
|
+
const refused = firstRefusal(SCOPE_SCHEMA, value);
|
|
76
|
+
if (refused !== undefined) {
|
|
77
|
+
throw refusal(file, dotted(placeIn(value, refused.pointer, refused.member)), refused.reason);
|
|
159
78
|
}
|
|
160
|
-
|
|
161
|
-
stringMember(value, "workspace", "");
|
|
79
|
+
const declared = value as { target: DeclaredModule; consumers?: readonly DeclaredModule[] };
|
|
162
80
|
const documentDir = dirnamePath(absolute);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (!Array.isArray(listed)) {
|
|
169
|
-
throw new ScopeError(`scope: ${file}: consumers is not an array`);
|
|
170
|
-
}
|
|
171
|
-
const consumers = (listed as unknown[]).map((entry, index) =>
|
|
172
|
-
readModule(entry, `consumers[${String(index)}]`, ROLE_CONSUMER, documentDir),
|
|
173
|
-
);
|
|
174
|
-
return { target, consumers };
|
|
81
|
+
const moduleOf = (module: DeclaredModule): Module => ({
|
|
82
|
+
id: module.id ?? "",
|
|
83
|
+
path: isAbsolutePath(module.path) ? module.path : joinPath(documentDir, module.path),
|
|
84
|
+
});
|
|
85
|
+
return { target: moduleOf(declared.target), consumers: (declared.consumers ?? []).map(moduleOf) };
|
|
175
86
|
}
|
|
176
87
|
|
|
177
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* The single-package scope one directory is, when no scope document is given. A target that is
|
|
90
|
+
* no directory holds no program.
|
|
91
|
+
*/
|
|
178
92
|
export function scopeForDir(host: Host, dir: string): Scope {
|
|
179
93
|
const path = resolvePath(host.workingDirectory(), dir);
|
|
180
94
|
const kind = host.kindOf(path);
|
|
181
|
-
if (kind === "absent") {
|
|
182
|
-
throw new ScopeError(`scope: ${path} does not exist`);
|
|
183
|
-
}
|
|
184
95
|
if (kind !== "directory") {
|
|
185
|
-
throw
|
|
96
|
+
throw failureExit(
|
|
97
|
+
"no-program",
|
|
98
|
+
`the target ${path} ${kind === "absent" ? "does not exist" : "is not a directory"}, so it holds no compiler configuration, no file named ${MARKERS.join(", ")}: name the directory that holds one`,
|
|
99
|
+
);
|
|
186
100
|
}
|
|
187
101
|
return { target: { id: "", path }, consumers: [] };
|
|
188
102
|
}
|
|
@@ -82,7 +82,7 @@ import { erasedStores, type ErasedStores } from "./erased-stores.ts";
|
|
|
82
82
|
import { checkedTypes, CONVERSION_MEMBERS, valueReach, type Reached } from "./value-reach.ts";
|
|
83
83
|
|
|
84
84
|
/** The serializer every run reads, whatever the configuration names. */
|
|
85
|
-
const JSON_STRINGIFY: DeclarationEntry = {
|
|
85
|
+
const JSON_STRINGIFY: DeclarationEntry = { global: "JSON.stringify" };
|
|
86
86
|
|
|
87
87
|
/** The methods a serializer and a formatter resolve by name on a value that left the analysis. */
|
|
88
88
|
const CONVERSIONS: ReadonlySet<string> = new Set(["toJSON", "toString"]);
|
package/src/session.ts
CHANGED
|
@@ -15,9 +15,7 @@ import type {
|
|
|
15
15
|
} from "@typescript/native/sync";
|
|
16
16
|
import type { Node, SourceFile } from "@typescript/native/ast";
|
|
17
17
|
import { componentLayer, mapperManifest } from "./component-layer.ts";
|
|
18
|
-
import { DiscoveryError } from "./discover.ts";
|
|
19
18
|
import type { Host, TemporaryDirectory } from "./host.ts";
|
|
20
|
-
import type { WorkspaceProgram } from "./workspace-programs.ts";
|
|
21
19
|
import { joinPath } from "./paths.ts";
|
|
22
20
|
import {
|
|
23
21
|
queriesOf,
|
|
@@ -37,8 +35,8 @@ import { MANIFEST } from "./fs/manifest.ts";
|
|
|
37
35
|
export interface Engine {
|
|
38
36
|
/**
|
|
39
37
|
* Resolves one compiler configuration file, or throws with the reason it cannot
|
|
40
|
-
* be read. The result is spelled through the client's own signature: the type
|
|
41
|
-
*
|
|
38
|
+
* be read. The result is spelled through the client's own signature: the type the
|
|
39
|
+
* compiler package returns here is not one of its exported names.
|
|
42
40
|
*/
|
|
43
41
|
parseConfigFile(file: DocumentIdentifier): ReturnType<API["parseConfigFile"]>;
|
|
44
42
|
/** Resolves one configuration's JSON as if `configFile` held it, which no file need. */
|
|
@@ -272,13 +270,6 @@ export interface ProjectView<Brand> {
|
|
|
272
270
|
*/
|
|
273
271
|
export type ProjectVisitor<Result> = <Brand>(project: ProjectView<Brand>) => Result;
|
|
274
272
|
|
|
275
|
-
/** One project a session opened: its configuration and the files of its own program. */
|
|
276
|
-
export interface OpenedFiles {
|
|
277
|
-
readonly configFile: string;
|
|
278
|
-
/** The program's source files no default library and no external library holds. */
|
|
279
|
-
readonly fileNames: () => readonly string[];
|
|
280
|
-
}
|
|
281
|
-
|
|
282
273
|
/**
|
|
283
274
|
* The source files of one program that `own` admits, in the program's order, none of
|
|
284
275
|
* them one of the compiler's default libraries.
|
|
@@ -472,19 +463,30 @@ export function diagnosticsOf<Brand>(project: ProjectView<Brand>): ProjectDiagno
|
|
|
472
463
|
};
|
|
473
464
|
}
|
|
474
465
|
|
|
466
|
+
/** How a snapshot builds one configuration other than by opening the project it names. */
|
|
467
|
+
export type ProgramBuild =
|
|
468
|
+
| { readonly kind: "program"; readonly params: CreateSnapshotProgramParams }
|
|
469
|
+
/** A configuration only the snapshot holds, at a path no file has. */
|
|
470
|
+
| { readonly kind: "configuration"; readonly file: string; readonly text: string };
|
|
471
|
+
|
|
475
472
|
/** One run's result: what the visitor answered for each project, and what it cost. */
|
|
476
473
|
export interface SessionResult<Result> {
|
|
477
|
-
/** One answer per configuration
|
|
474
|
+
/** One answer per configuration, in the order named. */
|
|
478
475
|
readonly projects: readonly Result[];
|
|
479
|
-
/** The configurations the snapshot opened no project for, in the order named. */
|
|
480
|
-
readonly unopened: readonly string[];
|
|
481
476
|
readonly timing: TimingInfo;
|
|
482
|
-
/** Every question the checker could not answer, each once, in the order first asked. */
|
|
483
|
-
readonly unanswered: readonly UnansweredQuestion[];
|
|
484
477
|
}
|
|
485
478
|
|
|
486
|
-
/**
|
|
487
|
-
|
|
479
|
+
/** One open snapshot's projects, valid only while the session that handed it out runs. */
|
|
480
|
+
export interface OpenedSession {
|
|
481
|
+
/** One view per configuration the snapshot opened a project for, in the order named. */
|
|
482
|
+
readonly views: readonly ProjectView<unknown>[];
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** How one session opens its configurations. */
|
|
486
|
+
export interface SessionOptions {
|
|
487
|
+
/** How the snapshot builds each configuration it does not open as its project. */
|
|
488
|
+
readonly programs?: ReadonlyMap<string, ProgramBuild> | undefined;
|
|
489
|
+
}
|
|
488
490
|
|
|
489
491
|
/**
|
|
490
492
|
* The projects one snapshot holds for the configurations named, by configuration file. A
|
|
@@ -513,22 +515,18 @@ function projectsOf(
|
|
|
513
515
|
}
|
|
514
516
|
|
|
515
517
|
/**
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
* `first` runs over every opened project before the first visit and `last` after the last,
|
|
521
|
-
* both while the client answers; the snapshot and the client are then released.
|
|
518
|
+
* Opens every project the compiler configurations name in one never updated snapshot of one
|
|
519
|
+
* client, and hands them to `use` while the client answers; the snapshot and the client are
|
|
520
|
+
* then released. A configuration `options.programs` describes is built as that program, or
|
|
521
|
+
* opened from the configuration it writes. One the snapshot holds nothing for has no view.
|
|
522
522
|
*/
|
|
523
|
-
export function
|
|
523
|
+
export function openSession<Result>(
|
|
524
524
|
engine: Engine,
|
|
525
525
|
configFiles: readonly string[],
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
first: (opened: readonly OpenedFiles[]) => void = () => undefined,
|
|
531
|
-
): SessionResult<Result> {
|
|
526
|
+
options: SessionOptions,
|
|
527
|
+
use: (opened: OpenedSession, unanswered: () => readonly UnansweredQuestion[]) => Result,
|
|
528
|
+
): Result {
|
|
529
|
+
const programs = options.programs ?? new Map<string, ProgramBuild>();
|
|
532
530
|
try {
|
|
533
531
|
const built: string[] = [];
|
|
534
532
|
const params: CreateSnapshotProgramParams[] = [];
|
|
@@ -553,32 +551,13 @@ export function runSession<Result>(
|
|
|
553
551
|
const opened = projectsOf(snapshot, built, written);
|
|
554
552
|
const log = new UnansweredLog();
|
|
555
553
|
const views: ProjectView<unknown>[] = [];
|
|
556
|
-
const missing: string[] = [];
|
|
557
554
|
for (const configFile of configFiles) {
|
|
558
555
|
const project = opened.get(configFile);
|
|
559
|
-
if (project
|
|
560
|
-
|
|
561
|
-
throw new DiscoveryError(`${configFile} opened no project`, []);
|
|
562
|
-
}
|
|
563
|
-
missing.push(configFile);
|
|
564
|
-
continue;
|
|
556
|
+
if (project !== undefined) {
|
|
557
|
+
views.push(viewOf(project, configFile, engine, log));
|
|
565
558
|
}
|
|
566
|
-
views.push(viewOf(project, configFile, engine, log));
|
|
567
559
|
}
|
|
568
|
-
|
|
569
|
-
views.map((view) => ({
|
|
570
|
-
configFile: view.configFile,
|
|
571
|
-
fileNames: () => view.ownSourceFiles().map((file) => file.fileName),
|
|
572
|
-
})),
|
|
573
|
-
);
|
|
574
|
-
const projects = views.map((view) => visit(view));
|
|
575
|
-
last();
|
|
576
|
-
return {
|
|
577
|
-
projects,
|
|
578
|
-
unopened: missing,
|
|
579
|
-
timing: engine.getTimingInfo(),
|
|
580
|
-
unanswered: log.questions,
|
|
581
|
-
};
|
|
560
|
+
return use({ views }, () => log.questions);
|
|
582
561
|
} finally {
|
|
583
562
|
snapshot.dispose();
|
|
584
563
|
}
|
|
@@ -586,3 +565,23 @@ export function runSession<Result>(
|
|
|
586
565
|
engine.close();
|
|
587
566
|
}
|
|
588
567
|
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Runs `visit` over the project {@link openSession} opens for each configuration, and answers
|
|
571
|
+
* in their order; a configuration the snapshot opens nothing for throws before any visit.
|
|
572
|
+
*/
|
|
573
|
+
export function runSession<Result>(
|
|
574
|
+
engine: Engine,
|
|
575
|
+
configFiles: readonly string[],
|
|
576
|
+
visit: ProjectVisitor<Result>,
|
|
577
|
+
): SessionResult<Result> {
|
|
578
|
+
return openSession(engine, configFiles, {}, (opened) => {
|
|
579
|
+
const unopened = configFiles.find(
|
|
580
|
+
(configFile) => !opened.views.some((view) => view.configFile === configFile),
|
|
581
|
+
);
|
|
582
|
+
if (unopened !== undefined) {
|
|
583
|
+
throw new Error(`${unopened} opened no project`);
|
|
584
|
+
}
|
|
585
|
+
return { projects: opened.views.map((view) => visit(view)), timing: engine.getTimingInfo() };
|
|
586
|
+
});
|
|
587
|
+
}
|
package/src/source-files.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { SourceFile } from "@typescript/native/ast";
|
|
8
|
-
import { commonDirectory, emittedFrom } from "./emit-map.ts";
|
|
8
|
+
import { commonDirectory, emittedFrom } from "./load/emit-map.ts";
|
|
9
9
|
import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
|
|
10
10
|
import type { ProjectView } from "./session.ts";
|
|
11
11
|
|
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 { isRecord, 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 {
|
|
@@ -185,30 +185,26 @@ function readDocument(
|
|
|
185
185
|
symbols: readonly InventorySymbol[],
|
|
186
186
|
shape: Shape,
|
|
187
187
|
): Suppressions {
|
|
188
|
-
const
|
|
189
|
-
if (
|
|
188
|
+
const text = readTargetDocument(host, targetRoot, shape.file);
|
|
189
|
+
if (text === undefined) {
|
|
190
190
|
return { records: [], refusals: [] };
|
|
191
191
|
}
|
|
192
|
-
const text = host.readFile(file);
|
|
193
192
|
const malformed = (detail: string, site = documentSite(shape)): never => {
|
|
194
193
|
throw new SuppressionError(shape.mechanism, siteText(site), detail, shape.documentWant);
|
|
195
194
|
};
|
|
196
|
-
let
|
|
195
|
+
let document;
|
|
197
196
|
try {
|
|
198
|
-
|
|
197
|
+
document = readStrict(text, shape.array);
|
|
199
198
|
} catch (error: unknown) {
|
|
200
|
-
|
|
199
|
+
if (!(error instanceof StrictJSONError)) {
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
return malformed(error.message, positionAt(text, shape.file, error.offset));
|
|
201
203
|
}
|
|
204
|
+
const parsed = document.value;
|
|
202
205
|
if (!isRecord(parsed)) {
|
|
203
206
|
return malformed(shape.file);
|
|
204
207
|
}
|
|
205
|
-
const walk = walkDocument(text, shape.array);
|
|
206
|
-
if (walk.twice !== undefined) {
|
|
207
|
-
malformed(
|
|
208
|
-
`the member ${JSON.stringify(walk.twice.name)} is written twice, so neither value is chosen`,
|
|
209
|
-
positionAt(text, shape.file, walk.twice.offset),
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
208
|
for (const key of Object.keys(parsed)) {
|
|
213
209
|
if (key !== "description" && key !== shape.array) {
|
|
214
210
|
malformed(`the member ${JSON.stringify(key)} is not one the grammar declares`);
|
|
@@ -224,7 +220,7 @@ function readDocument(
|
|
|
224
220
|
const records: SuppressionRecord[] = [];
|
|
225
221
|
const refusals: Refusal[] = [];
|
|
226
222
|
(held as readonly unknown[]).forEach((value, index) => {
|
|
227
|
-
const offset =
|
|
223
|
+
const offset = document.starts.get(index);
|
|
228
224
|
const site = offset === undefined ? documentSite(shape) : positionAt(text, shape.file, offset);
|
|
229
225
|
const read = readRecord(value, site, byReference, shape);
|
|
230
226
|
records.push(...read.records);
|
|
@@ -382,10 +378,9 @@ function* separatorsEscaped(pieces: Iterable<string>): Generator<string, void, u
|
|
|
382
378
|
* A baseline holding one row per finding, in the order given, which is the report's, and
|
|
383
379
|
* the other language's rows `kept`, Go's rows ahead of TypeScript's and each scheme's in
|
|
384
380
|
* the order given, so two analyzers writing one document in either order write the same
|
|
385
|
-
* bytes, in the form grammar/suppression.md fixes. Every
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
* 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.
|
|
389
384
|
*/
|
|
390
385
|
export function writeBaseline(
|
|
391
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/template-field.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
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";
|
|
@@ -22,6 +22,7 @@ import { extensionOf, isAbsolutePath, joinPath, normalizePath, relativePath } fr
|
|
|
22
22
|
import { isComponentFile, type Position } from "./position.ts";
|
|
23
23
|
import { bytewise } from "./text/compare.ts";
|
|
24
24
|
import { filesBelow } from "./fs/tree.ts";
|
|
25
|
+
import { UsageError } from "./usage.ts";
|
|
25
26
|
|
|
26
27
|
/** One template, by its path below the target root. */
|
|
27
28
|
interface TemplateFile {
|
|
@@ -60,7 +61,11 @@ const IDENTIFIER = /[\p{ID_Start}$_][\p{ID_Continue}$\u200C\u200D]*/uy;
|
|
|
60
61
|
* exemption it was configured for off. A symbolic link to a directory is not
|
|
61
62
|
* followed.
|
|
62
63
|
*/
|
|
63
|
-
export function readTemplates(
|
|
64
|
+
export function readTemplates(
|
|
65
|
+
host: Host,
|
|
66
|
+
targetRoot: string,
|
|
67
|
+
analysis: ResolvedConfig["analysis"],
|
|
68
|
+
): Templates {
|
|
64
69
|
const files = new Map<string, string>();
|
|
65
70
|
const read = (dir: string): void => {
|
|
66
71
|
for (const below of filesBelow(host, joinPath(targetRoot, dir), () => true)) {
|
|
@@ -71,7 +76,7 @@ export function readTemplates(host: Host, targetRoot: string, analysis: Analysis
|
|
|
71
76
|
}
|
|
72
77
|
}
|
|
73
78
|
};
|
|
74
|
-
for (const configured of analysis.
|
|
79
|
+
for (const configured of analysis.template_dirs) {
|
|
75
80
|
const dir = normalizePath(configured);
|
|
76
81
|
if (
|
|
77
82
|
isAbsolutePath(dir) ||
|
|
@@ -79,16 +84,14 @@ export function readTemplates(host: Host, targetRoot: string, analysis: Analysis
|
|
|
79
84
|
dir.startsWith("../") ||
|
|
80
85
|
host.kindOf(joinPath(targetRoot, dir)) !== "directory"
|
|
81
86
|
) {
|
|
82
|
-
throw new
|
|
83
|
-
"malformed",
|
|
84
|
-
"analysis.template_dirs",
|
|
87
|
+
throw new UsageError(
|
|
85
88
|
`analysis.template_dirs: ${JSON.stringify(configured)} is not a directory below the target root`,
|
|
86
89
|
);
|
|
87
90
|
}
|
|
88
91
|
read(dir);
|
|
89
92
|
}
|
|
90
93
|
return {
|
|
91
|
-
delimiters: analysis.
|
|
94
|
+
delimiters: analysis.template_delimiters,
|
|
92
95
|
files: [...files]
|
|
93
96
|
.map(([path, text]) => ({ path, text }))
|
|
94
97
|
.sort((a, b) => bytewise(a.path, b.path)),
|