@cplieger/deadset-ts 8.0.0-dev.7 → 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/node-host.js +21 -3
- package/dist/src/analysis.js +13 -14
- package/dist/src/config.js +6 -83
- 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/report.generated.js +3 -2
- package/dist/src/discover.js +1 -1
- 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/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 +59 -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/print.js +10 -79
- package/dist/src/report-schema.js +3 -224
- package/dist/src/resolve.js +245 -715
- package/dist/src/run.js +48 -55
- package/dist/src/schema.js +320 -344
- package/dist/src/scope.js +33 -101
- package/dist/src/serialization-contract.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/usage.js +10 -0
- package/dist/src/verbs/analyze.js +12 -15
- package/dist/src/verbs/explain.js +3 -3
- package/dist/src/verbs/print-projects.js +2 -2
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +30 -26
- package/src/config.ts +31 -298
- package/src/configured-declarations.ts +18 -23
- package/src/consumers.ts +4 -4
- package/src/contract/config.generated.ts +798 -0
- package/src/contract/report.generated.ts +3 -2
- package/src/discover.ts +1 -1
- package/src/edges.ts +39 -124
- package/src/exempt-classes.ts +0 -5
- package/src/exempt.ts +4 -4
- 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 +74 -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/print.ts +11 -92
- package/src/report-schema.ts +3 -299
- package/src/resolve.ts +281 -1105
- package/src/run.ts +50 -63
- package/src/schema.ts +391 -389
- package/src/scope.ts +39 -119
- package/src/serialization-contract.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/usage.ts +10 -0
- package/src/verbs/analyze.ts +12 -23
- package/src/verbs/explain.ts +3 -7
- package/src/verbs/print-projects.ts +2 -2
- package/src/verbs/verb.ts +2 -2
package/src/scope.ts
CHANGED
|
@@ -1,13 +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 { checkDocument, type KeyNode } from "./schema.ts";
|
|
5
|
-
import { isRecord } from "./json-document.ts";
|
|
6
5
|
import { message } from "./product.ts";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const ROLE_TARGET = "target";
|
|
10
|
-
const ROLE_CONSUMER = "consumer";
|
|
6
|
+
import { firstRefusal } from "./schema.ts";
|
|
7
|
+
import { UsageError } from "./usage.ts";
|
|
11
8
|
|
|
12
9
|
/**
|
|
13
10
|
* A scope document above this length is refused rather than decoded. It names one
|
|
@@ -16,8 +13,14 @@ const ROLE_CONSUMER = "consumer";
|
|
|
16
13
|
const MAX_DOCUMENT_LENGTH = 1 << 20;
|
|
17
14
|
|
|
18
15
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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.
|
|
21
24
|
*/
|
|
22
25
|
export class ScopeError extends Error {
|
|
23
26
|
constructor(message: string) {
|
|
@@ -27,12 +30,8 @@ export class ScopeError extends Error {
|
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
/**
|
|
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.
|
|
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.
|
|
36
35
|
*/
|
|
37
36
|
export interface Module {
|
|
38
37
|
/** The package name. A document may state it; otherwise it is empty. */
|
|
@@ -47,131 +46,52 @@ export interface Scope {
|
|
|
47
46
|
readonly consumers: readonly Module[];
|
|
48
47
|
}
|
|
49
48
|
|
|
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
|
-
};
|
|
49
|
+
interface DeclaredModule {
|
|
50
|
+
readonly id?: string;
|
|
51
|
+
readonly path: string;
|
|
106
52
|
}
|
|
107
53
|
|
|
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
|
-
}
|
|
54
|
+
function refusal(file: string, place: string, reason: string): ScopeError {
|
|
55
|
+
return new ScopeError(`scope: ${file}: ${place === "" ? "" : `${place}: `}${reason}`);
|
|
128
56
|
}
|
|
129
57
|
|
|
130
58
|
/**
|
|
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.
|
|
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.
|
|
139
63
|
*/
|
|
140
64
|
export function readScope(host: Host, file: string): Scope {
|
|
141
65
|
const absolute = resolvePath(host.workingDirectory(), file);
|
|
142
66
|
let text: string;
|
|
143
67
|
try {
|
|
144
|
-
text = host.readFile(absolute);
|
|
68
|
+
text = host.readFile(absolute, MAX_DOCUMENT_BYTES);
|
|
145
69
|
} catch (error: unknown) {
|
|
146
|
-
throw new
|
|
70
|
+
throw new UsageError(`scope: ${file}: ${message(error)}`);
|
|
147
71
|
}
|
|
148
72
|
if (text.length > MAX_DOCUMENT_LENGTH) {
|
|
149
73
|
throw new ScopeError(`scope: ${file} is longer than ${String(MAX_DOCUMENT_LENGTH)} characters`);
|
|
150
74
|
}
|
|
151
75
|
let value: unknown;
|
|
152
76
|
try {
|
|
153
|
-
value =
|
|
77
|
+
value = readStrict(text).value;
|
|
154
78
|
} catch (error: unknown) {
|
|
155
|
-
|
|
79
|
+
if (!(error instanceof StrictJSONError)) {
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
throw refusal(file, dotted(error.place), locatedFault(text, error));
|
|
156
83
|
}
|
|
157
|
-
|
|
158
|
-
|
|
84
|
+
const refused = firstRefusal(SCOPE_SCHEMA, value);
|
|
85
|
+
if (refused !== undefined) {
|
|
86
|
+
throw refusal(file, dotted(placeIn(value, refused.pointer, refused.member)), refused.reason);
|
|
159
87
|
}
|
|
160
|
-
|
|
161
|
-
stringMember(value, "workspace", "");
|
|
88
|
+
const declared = value as { target: DeclaredModule; consumers?: readonly DeclaredModule[] };
|
|
162
89
|
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 };
|
|
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) };
|
|
175
95
|
}
|
|
176
96
|
|
|
177
97
|
/** The single-package scope one directory is, when no scope document is given. */
|
|
@@ -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/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)),
|
package/src/usage.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A refusal of the invocation: the fix is to the command line or to a document it
|
|
3
|
+
* names, so the run ends with the usage code before any analysis.
|
|
4
|
+
*/
|
|
5
|
+
export class UsageError extends Error {
|
|
6
|
+
constructor(message: string) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "UsageError";
|
|
9
|
+
}
|
|
10
|
+
}
|
package/src/verbs/analyze.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type RunAnalysis,
|
|
13
13
|
type RunProject,
|
|
14
14
|
} from "../analysis.ts";
|
|
15
|
-
import {
|
|
15
|
+
import { kindOf } from "../config.ts";
|
|
16
16
|
import { CONFORMANCE, DECLARED_GAPS } from "../conformance.ts";
|
|
17
17
|
import { moduleIdentity } from "../consumers.ts";
|
|
18
18
|
import { analyzerProvenance, verdictOf } from "../findings-pass.ts";
|
|
@@ -38,8 +38,9 @@ import { resolve } from "../resolve.ts";
|
|
|
38
38
|
import { otherSchemeRows, rowReason, writeBaseline, type Recorded } from "../suppress-file.ts";
|
|
39
39
|
import type { UnseenLoad } from "../unseen-loads.ts";
|
|
40
40
|
import { type Verb, type VerbOptions } from "./verb.ts";
|
|
41
|
+
import { UsageError } from "../usage.ts";
|
|
41
42
|
import { bytewise } from "../text/compare.ts";
|
|
42
|
-
import { diagnose,
|
|
43
|
+
import { diagnose, message } from "../product.ts";
|
|
43
44
|
|
|
44
45
|
/** The options `analyze` takes beside the ones every verb takes. */
|
|
45
46
|
export const ANALYZE_OPTIONS: VerbOptions = {
|
|
@@ -70,14 +71,12 @@ function formatsOf(named: readonly string[], configured: readonly Format[]): rea
|
|
|
70
71
|
const seen = new Set<string>();
|
|
71
72
|
for (const format of formats) {
|
|
72
73
|
if (!RENDERINGS.has(format as Format)) {
|
|
73
|
-
throw new
|
|
74
|
-
"malformed",
|
|
75
|
-
"",
|
|
74
|
+
throw new UsageError(
|
|
76
75
|
`${JSON.stringify(format)} is not a format this analyzer renders: the formats are ${formatNames()}`,
|
|
77
76
|
);
|
|
78
77
|
}
|
|
79
78
|
if (seen.has(format)) {
|
|
80
|
-
throw new
|
|
79
|
+
throw new UsageError(`the format ${JSON.stringify(format)} is named twice`);
|
|
81
80
|
}
|
|
82
81
|
seen.add(format);
|
|
83
82
|
}
|
|
@@ -189,9 +188,7 @@ function templateOf(
|
|
|
189
188
|
): Template | undefined {
|
|
190
189
|
if (named === undefined || named === "") {
|
|
191
190
|
if (formats.includes("template")) {
|
|
192
|
-
throw new
|
|
193
|
-
"malformed",
|
|
194
|
-
"",
|
|
191
|
+
throw new UsageError(
|
|
195
192
|
"the template format renders the template --template names, and none was named",
|
|
196
193
|
);
|
|
197
194
|
}
|
|
@@ -201,7 +198,7 @@ function templateOf(
|
|
|
201
198
|
try {
|
|
202
199
|
source = host.readFile(resolvePath(invoked, named));
|
|
203
200
|
} catch (error: unknown) {
|
|
204
|
-
throw new
|
|
201
|
+
throw new UsageError(`--template=${named}: ${message(error)}`);
|
|
205
202
|
}
|
|
206
203
|
try {
|
|
207
204
|
return parseTemplate(source);
|
|
@@ -209,7 +206,7 @@ function templateOf(
|
|
|
209
206
|
if (!(error instanceof TemplateError)) {
|
|
210
207
|
throw error;
|
|
211
208
|
}
|
|
212
|
-
throw new
|
|
209
|
+
throw new UsageError(`--template=${named}: ${error.message}`);
|
|
213
210
|
}
|
|
214
211
|
}
|
|
215
212
|
|
|
@@ -231,25 +228,17 @@ interface Documents {
|
|
|
231
228
|
export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option, repeated }) => {
|
|
232
229
|
const reportPath = option("report");
|
|
233
230
|
if (reportPath === undefined || reportPath === "") {
|
|
234
|
-
throw new
|
|
235
|
-
"malformed",
|
|
236
|
-
"",
|
|
231
|
+
throw new UsageError(
|
|
237
232
|
"analyze writes its report to the path --report names, and no path was named",
|
|
238
233
|
);
|
|
239
234
|
}
|
|
240
235
|
const exitCode = option("exit-code") ?? EXIT_CODE_ON;
|
|
241
236
|
if (exitCode !== EXIT_CODE_ON && exitCode !== EXIT_CODE_OFF) {
|
|
242
|
-
throw new
|
|
243
|
-
"malformed",
|
|
244
|
-
"",
|
|
237
|
+
throw new UsageError(
|
|
245
238
|
`--exit-code=${JSON.stringify(exitCode)}: the values are ${EXIT_CODE_ON} and ${EXIT_CODE_OFF}`,
|
|
246
239
|
);
|
|
247
240
|
}
|
|
248
241
|
const { config, provenance } = resolve(inputs());
|
|
249
|
-
const kind = config.targetKind;
|
|
250
|
-
if (kind === "") {
|
|
251
|
-
throw new ConfigError("missing-target-kind", "target.kind", "target.kind is not set");
|
|
252
|
-
}
|
|
253
242
|
const invoked = host.workingDirectory();
|
|
254
243
|
const baseline = option("baseline-write");
|
|
255
244
|
const documents: Documents = {
|
|
@@ -318,7 +307,7 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
|
|
|
318
307
|
conformance: CONFORMANCE,
|
|
319
308
|
declaredGaps: DECLARED_GAPS,
|
|
320
309
|
target: {
|
|
321
|
-
kind: kindOf(kind
|
|
310
|
+
kind: kindOf(config.target.kind),
|
|
322
311
|
root,
|
|
323
312
|
identity: moduleIdentity(host, scoped.target),
|
|
324
313
|
},
|
|
@@ -340,7 +329,7 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
|
|
|
340
329
|
...unseenLoadNotes(run.unseenLoads),
|
|
341
330
|
],
|
|
342
331
|
});
|
|
343
|
-
report = capFindings(sortFindings(built, config.reporters.sort), config.reporters.
|
|
332
|
+
report = capFindings(sortFindings(built, config.reporters.sort), config.reporters.max_findings);
|
|
344
333
|
verdict = verdictOf(result, config);
|
|
345
334
|
} catch (error: unknown) {
|
|
346
335
|
if (!(error instanceof ReportPathError)) {
|
package/src/verbs/explain.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { EXIT_CLEAN, EXIT_USAGE } from "../contract/contract.generated.ts";
|
|
|
2
2
|
import { runAnalysis, type RunAnalysis } from "../analysis.ts";
|
|
3
3
|
import { listing } from "../components.ts";
|
|
4
4
|
import type { Cascade } from "../config.ts";
|
|
5
|
-
import { ConfigError } from "../config.ts";
|
|
6
5
|
import type { CompletedFinding } from "../finding.ts";
|
|
7
6
|
import { OUTSIDE, swept as judged } from "../graph.ts";
|
|
8
7
|
import type { InventorySymbol } from "../inventory.ts";
|
|
@@ -14,6 +13,7 @@ import type { Reference } from "../references.ts";
|
|
|
14
13
|
import { resolve } from "../resolve.ts";
|
|
15
14
|
import type { Root } from "../roots.ts";
|
|
16
15
|
import { type Verb, type VerbOptions } from "./verb.ts";
|
|
16
|
+
import { UsageError } from "../usage.ts";
|
|
17
17
|
import { bytewise } from "../text/compare.ts";
|
|
18
18
|
import { diagnose } from "../product.ts";
|
|
19
19
|
|
|
@@ -47,16 +47,12 @@ function namedSymbol(option: (name: string) => string | undefined): string {
|
|
|
47
47
|
});
|
|
48
48
|
const [only, ...more] = named;
|
|
49
49
|
if (only === undefined) {
|
|
50
|
-
throw new
|
|
51
|
-
"malformed",
|
|
52
|
-
"",
|
|
50
|
+
throw new UsageError(
|
|
53
51
|
"explain explains the symbol --why, --why-live or --why-not names, and none was named",
|
|
54
52
|
);
|
|
55
53
|
}
|
|
56
54
|
if (more.length > 0) {
|
|
57
|
-
throw new
|
|
58
|
-
"malformed",
|
|
59
|
-
"",
|
|
55
|
+
throw new UsageError(
|
|
60
56
|
`explain explains one symbol, and ${named.map((one) => `--${one.name}`).join(", ")} each name one`,
|
|
61
57
|
);
|
|
62
58
|
}
|
|
@@ -2,7 +2,7 @@ import { EXIT_CLEAN } from "../contract/contract.generated.ts";
|
|
|
2
2
|
import { runProjects } from "../analysis.ts";
|
|
3
3
|
import { partialNotes } from "../partial.ts";
|
|
4
4
|
import { resolvePath } from "../paths.ts";
|
|
5
|
-
import {
|
|
5
|
+
import { resolvePartial } from "../resolve.ts";
|
|
6
6
|
import { type Verb } from "./verb.ts";
|
|
7
7
|
import { diagnose } from "../product.ts";
|
|
8
8
|
|
|
@@ -14,7 +14,7 @@ import { diagnose } from "../product.ts";
|
|
|
14
14
|
* instead, and one that fails the analysis fails this command the same way.
|
|
15
15
|
*/
|
|
16
16
|
export const printProjectsVerb: Verb = ({ out, err, host, inputs, scope, openClient }) => {
|
|
17
|
-
const config =
|
|
17
|
+
const config = resolvePartial(inputs());
|
|
18
18
|
const scoped = scope();
|
|
19
19
|
const answer = runProjects(openClient(config), host, scoped, config);
|
|
20
20
|
for (const id of answer.configurations) {
|
package/src/verbs/verb.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Inputs, PartialConfig } from "../config.ts";
|
|
2
2
|
import type { Host } from "../host.ts";
|
|
3
3
|
import type { Writer } from "../run.ts";
|
|
4
4
|
import type { Scope } from "../scope.ts";
|
|
@@ -21,7 +21,7 @@ interface Invocation {
|
|
|
21
21
|
* Opens the compiler client for a run under `config`. A client the verb opened is
|
|
22
22
|
* closed if the verb throws.
|
|
23
23
|
*/
|
|
24
|
-
readonly openClient: (config:
|
|
24
|
+
readonly openClient: (config: PartialConfig) => Engine;
|
|
25
25
|
/** The arguments after the verb's name, as written. */
|
|
26
26
|
readonly args: readonly string[];
|
|
27
27
|
/** The last value one of the verb's own options was given, undefined where none was. */
|