@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
|
@@ -1,106 +1,228 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A
|
|
3
|
-
*
|
|
2
|
+
* A text that is not one strict JSON document: a syntax error, data after the value, a
|
|
3
|
+
* member one object writes twice, or nesting past {@link MAX_DEPTH}. `place` is the
|
|
4
|
+
* value the fault is in, followed by the member written twice.
|
|
4
5
|
*/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
export class StrictJSONError extends Error {
|
|
7
|
+
offset;
|
|
8
|
+
place;
|
|
9
|
+
constructor(reason, offset, place) {
|
|
10
|
+
super(reason);
|
|
11
|
+
this.name = "StrictJSONError";
|
|
12
|
+
this.offset = offset;
|
|
13
|
+
this.place = place;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* How deep values may nest. A document this analyzer reads nests a few levels, and a
|
|
18
|
+
* bound turns a document built to exhaust the stack into a refusal.
|
|
19
|
+
*/
|
|
20
|
+
const MAX_DEPTH = 1000;
|
|
21
|
+
const SPACE = /[ \t\n\r]*/uy;
|
|
22
|
+
const STRING = /"(?:[ !#$%&'()*+,\-./0-9:;<=>?@A-Z[\]-\u{10FFFF}]|\\(?:["\\/bfnrt]|u[0-9A-Fa-f]{4}))*"/uy;
|
|
23
|
+
const NUMBER = /-?(?:0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?/uy;
|
|
24
|
+
const LITERALS = new Map([
|
|
25
|
+
["true", true],
|
|
26
|
+
["false", false],
|
|
27
|
+
["null", null],
|
|
28
|
+
]);
|
|
29
|
+
/** One member name spelled as a JSON Pointer token. */
|
|
30
|
+
const escaped = (name) => name.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
31
|
+
/** The member names and indexes a JSON Pointer holds, unescaped. */
|
|
32
|
+
export function pointerSegments(pointer) {
|
|
33
|
+
return pointer === ""
|
|
34
|
+
? []
|
|
35
|
+
: pointer
|
|
36
|
+
.slice(1)
|
|
37
|
+
.split("/")
|
|
38
|
+
.map((token) => token.replaceAll("~1", "/").replaceAll("~0", "~"));
|
|
39
|
+
}
|
|
40
|
+
/** The JSON Pointer of a place written as segments. */
|
|
41
|
+
export function pointerOf(segments) {
|
|
42
|
+
return segments.map((segment) => `/${escaped(segment)}`).join("");
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The place a JSON Pointer names inside `value`, followed by `member` where it is not
|
|
46
|
+
* empty: a pointer's text does not say whether a segment of digits is an index, so the
|
|
47
|
+
* value it points into does.
|
|
48
|
+
*/
|
|
49
|
+
export function placeIn(value, pointer, member = "") {
|
|
50
|
+
const place = [];
|
|
51
|
+
let held = value;
|
|
52
|
+
for (const segment of pointerSegments(pointer)) {
|
|
53
|
+
if (Array.isArray(held)) {
|
|
54
|
+
place.push(Number(segment));
|
|
55
|
+
held = held[Number(segment)];
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
place.push(segment);
|
|
59
|
+
held = isRecord(held) ? held[segment] : undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return member === "" ? place : [...place, member];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A place as a refusal names it: member names joined by full stops and an index in
|
|
66
|
+
* square brackets, as `ts.serializers[0].returns`.
|
|
67
|
+
*/
|
|
68
|
+
export function dotted(place) {
|
|
69
|
+
let path = "";
|
|
70
|
+
for (const segment of place) {
|
|
71
|
+
path +=
|
|
72
|
+
typeof segment === "number" ? `[${String(segment)}]` : path === "" ? segment : `.${segment}`;
|
|
73
|
+
}
|
|
74
|
+
return path;
|
|
75
|
+
}
|
|
76
|
+
class Reader {
|
|
12
77
|
at = 0;
|
|
13
|
-
|
|
78
|
+
path = [];
|
|
79
|
+
fractions = new Map();
|
|
80
|
+
starts = new Map();
|
|
81
|
+
text;
|
|
82
|
+
records;
|
|
83
|
+
constructor(text, records) {
|
|
14
84
|
this.text = text;
|
|
15
|
-
this.
|
|
85
|
+
this.records = records;
|
|
16
86
|
}
|
|
17
|
-
|
|
18
|
-
this.value(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
this.at += 1;
|
|
87
|
+
read() {
|
|
88
|
+
const value = this.value(0);
|
|
89
|
+
this.match(SPACE);
|
|
90
|
+
if (this.at < this.text.length) {
|
|
91
|
+
this.fail("data follows the document's value");
|
|
23
92
|
}
|
|
93
|
+
return value;
|
|
24
94
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
95
|
+
fail(reason, offset = this.at, member = "") {
|
|
96
|
+
throw new StrictJSONError(reason, offset, member === "" ? [...this.path] : [...this.path, member]);
|
|
97
|
+
}
|
|
98
|
+
match(pattern) {
|
|
99
|
+
pattern.lastIndex = this.at;
|
|
100
|
+
const found = pattern.exec(this.text)?.[0];
|
|
101
|
+
this.at = found === undefined ? this.at : pattern.lastIndex;
|
|
102
|
+
return found;
|
|
103
|
+
}
|
|
104
|
+
expect(char, what) {
|
|
105
|
+
this.match(SPACE);
|
|
106
|
+
if (this.text[this.at] !== char) {
|
|
107
|
+
this.fail(`${what} is expected`);
|
|
30
108
|
}
|
|
31
109
|
this.at += 1;
|
|
32
|
-
return JSON.parse(this.text.slice(start, this.at));
|
|
33
110
|
}
|
|
34
|
-
value(
|
|
35
|
-
this.
|
|
36
|
-
if (
|
|
37
|
-
this.
|
|
111
|
+
value(depth) {
|
|
112
|
+
this.match(SPACE);
|
|
113
|
+
if (depth > MAX_DEPTH) {
|
|
114
|
+
this.fail(`values nest deeper than ${String(MAX_DEPTH)} levels`);
|
|
38
115
|
}
|
|
39
116
|
const char = this.text[this.at];
|
|
40
117
|
if (char === "{") {
|
|
41
|
-
this.object(
|
|
118
|
+
return this.object(depth);
|
|
42
119
|
}
|
|
43
|
-
|
|
44
|
-
this.
|
|
120
|
+
if (char === "[") {
|
|
121
|
+
return this.array(depth);
|
|
45
122
|
}
|
|
46
|
-
|
|
47
|
-
|
|
123
|
+
const string = this.match(STRING);
|
|
124
|
+
if (string !== undefined) {
|
|
125
|
+
return JSON.parse(string);
|
|
48
126
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
127
|
+
NUMBER.lastIndex = this.at;
|
|
128
|
+
const number = NUMBER.exec(this.text);
|
|
129
|
+
if (number !== null) {
|
|
130
|
+
this.at = NUMBER.lastIndex;
|
|
131
|
+
if (number[1] !== undefined || number[2] !== undefined) {
|
|
132
|
+
this.fractions.set(pointerOf(this.path.map(String)), number[0]);
|
|
133
|
+
}
|
|
134
|
+
return Number(number[0]);
|
|
135
|
+
}
|
|
136
|
+
for (const [literal, held] of LITERALS) {
|
|
137
|
+
if (this.text.startsWith(literal, this.at)) {
|
|
138
|
+
this.at += literal.length;
|
|
139
|
+
return held;
|
|
52
140
|
}
|
|
53
141
|
}
|
|
142
|
+
return this.fail(char === undefined ? "the document ends where a value is expected" : "a value is expected");
|
|
54
143
|
}
|
|
55
|
-
object(
|
|
56
|
-
const
|
|
144
|
+
object(depth) {
|
|
145
|
+
const held = {};
|
|
57
146
|
this.at += 1;
|
|
147
|
+
this.match(SPACE);
|
|
148
|
+
if (this.text[this.at] === "}") {
|
|
149
|
+
this.at += 1;
|
|
150
|
+
return held;
|
|
151
|
+
}
|
|
58
152
|
for (;;) {
|
|
59
|
-
this.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
153
|
+
this.match(SPACE);
|
|
154
|
+
const offset = this.at;
|
|
155
|
+
const spelled = this.match(STRING);
|
|
156
|
+
if (spelled === undefined) {
|
|
157
|
+
return this.fail("a member name is expected");
|
|
63
158
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
this.
|
|
159
|
+
const name = JSON.parse(spelled);
|
|
160
|
+
if (Object.hasOwn(held, name)) {
|
|
161
|
+
this.fail(`the member ${JSON.stringify(name)} is written twice, so neither value is chosen`, offset, name);
|
|
67
162
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
this.
|
|
163
|
+
this.expect(":", "a colon after the member name");
|
|
164
|
+
if (depth === 1 && this.path[0] === this.records) {
|
|
165
|
+
this.match(SPACE);
|
|
166
|
+
this.starts.set(name, this.at);
|
|
72
167
|
}
|
|
73
|
-
|
|
74
|
-
|
|
168
|
+
this.path.push(name);
|
|
169
|
+
Object.defineProperty(held, name, {
|
|
170
|
+
value: this.value(depth + 1),
|
|
171
|
+
enumerable: true,
|
|
172
|
+
writable: true,
|
|
173
|
+
configurable: true,
|
|
174
|
+
});
|
|
175
|
+
this.path.pop();
|
|
176
|
+
this.match(SPACE);
|
|
177
|
+
const next = this.text[this.at];
|
|
75
178
|
this.at += 1;
|
|
76
|
-
|
|
179
|
+
if (next === "}") {
|
|
180
|
+
return held;
|
|
181
|
+
}
|
|
182
|
+
if (next !== ",") {
|
|
183
|
+
this.at -= 1;
|
|
184
|
+
this.fail("a comma or a closing brace is expected");
|
|
185
|
+
}
|
|
77
186
|
}
|
|
78
187
|
}
|
|
79
|
-
|
|
188
|
+
array(depth) {
|
|
189
|
+
const held = [];
|
|
190
|
+
const records = depth === 1 && this.path[0] === this.records;
|
|
80
191
|
this.at += 1;
|
|
192
|
+
this.match(SPACE);
|
|
193
|
+
if (this.text[this.at] === "]") {
|
|
194
|
+
this.at += 1;
|
|
195
|
+
return held;
|
|
196
|
+
}
|
|
81
197
|
for (;;) {
|
|
82
|
-
this.
|
|
83
|
-
if (
|
|
84
|
-
this.at
|
|
85
|
-
|
|
198
|
+
this.match(SPACE);
|
|
199
|
+
if (records) {
|
|
200
|
+
this.starts.set(held.length, this.at);
|
|
201
|
+
}
|
|
202
|
+
this.path.push(held.length);
|
|
203
|
+
held.push(this.value(depth + 1));
|
|
204
|
+
this.path.pop();
|
|
205
|
+
this.match(SPACE);
|
|
206
|
+
const next = this.text[this.at];
|
|
207
|
+
this.at += 1;
|
|
208
|
+
if (next === "]") {
|
|
209
|
+
return held;
|
|
86
210
|
}
|
|
87
|
-
if (
|
|
88
|
-
this.at
|
|
211
|
+
if (next !== ",") {
|
|
212
|
+
this.at -= 1;
|
|
213
|
+
this.fail("a comma or a closing bracket is expected");
|
|
89
214
|
}
|
|
90
|
-
this.value(element);
|
|
91
215
|
}
|
|
92
216
|
}
|
|
93
217
|
}
|
|
94
218
|
/**
|
|
95
|
-
*
|
|
96
|
-
* records
|
|
97
|
-
* later value silently replacing the earlier one, so a document holding one is one a
|
|
98
|
-
* reader names rather than reads.
|
|
219
|
+
* Reads `text` as one strict JSON document, or throws a {@link StrictJSONError}.
|
|
220
|
+
* `records` names the top-level member whose values' offsets the read keeps.
|
|
99
221
|
*/
|
|
100
|
-
export function
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
return {
|
|
222
|
+
export function readStrict(text, records) {
|
|
223
|
+
const reader = new Reader(text, records);
|
|
224
|
+
const value = reader.read();
|
|
225
|
+
return { value, fractions: reader.fractions, starts: reader.starts };
|
|
104
226
|
}
|
|
105
227
|
/**
|
|
106
228
|
* One offset of a document as a finding carries it: the line counted from one, and the
|
|
@@ -111,6 +233,11 @@ export function positionAt(text, file, offset) {
|
|
|
111
233
|
const lineStart = before.lastIndexOf("\n") + 1;
|
|
112
234
|
return { path: file, line: before.split("\n").length, column: offset - lineStart + 1 };
|
|
113
235
|
}
|
|
236
|
+
/** A strict read's fault with the line and column it is at, as a refusal states it. */
|
|
237
|
+
export function locatedFault(text, error) {
|
|
238
|
+
const { line, column } = positionAt(text, "", error.offset);
|
|
239
|
+
return `line ${String(line)}, column ${String(column)}: ${error.message}`;
|
|
240
|
+
}
|
|
114
241
|
/** Whether a decoded JSON value is an object: not `null` and not an array. */
|
|
115
242
|
export function isRecord(value) {
|
|
116
243
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
package/dist/src/kinds.js
CHANGED
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
import { KIND_ROWS } from "./contract/contract.generated.js";
|
|
2
2
|
/** Every live row of the issue-kind vocabulary, by its code, in ascending code order. */
|
|
3
3
|
export const KINDS = new Map(KIND_ROWS.map((one) => [one.code, one]));
|
|
4
|
-
/**
|
|
5
|
-
* The code of every live issue kind, in ascending order. A severity key names one of
|
|
6
|
-
* them or a family prefix whose range holds one; a key naming a retired code, an
|
|
7
|
-
* unassigned code or a range holding no live kind is an unimplemented key, so every
|
|
8
|
-
* severity a configuration sets is one some analyzer reports under.
|
|
9
|
-
*/
|
|
10
|
-
export const LIVE_CODES = KIND_ROWS.map((one) => one.code);
|
|
11
4
|
/** The issue-kind codes whose severity the Contract fixes, in ascending order. */
|
|
12
5
|
export const FIXED_SEVERITY_CODES = KIND_ROWS.filter((one) => one.fixed).map((one) => one.code);
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { conventionFailuresFor } from "../conventions.js";
|
|
2
|
+
import { relativePath, resolvePath } from "../paths.js";
|
|
3
|
+
import { diagnosticsOf, ownedAs } from "../session.js";
|
|
4
|
+
import { skipAt } from "../type-errors.js";
|
|
5
|
+
import { classOf, MISSING_GLOBAL_TYPE, projectErrors, renderDiagnostic, } from "./diagnostics.js";
|
|
6
|
+
import { targetRelative } from "./discover.js";
|
|
7
|
+
import { missingModule, pathsOf } from "./expected-module.js";
|
|
8
|
+
import { FailureExit, failureLine } from "../failure.js";
|
|
9
|
+
import { ownErrors } from "./workspace-run.js";
|
|
10
|
+
/** What a failure ends or drops, by where the configuration comes from. */
|
|
11
|
+
const POLICY = {
|
|
12
|
+
declared: { load: "end", setup: "end", unopened: "end" },
|
|
13
|
+
scope: { load: "end", setup: "end", unopened: "end" },
|
|
14
|
+
derived: { load: "drop", setup: "drop", unopened: "drop" },
|
|
15
|
+
consumer: { load: "end", setup: "end", unopened: "end" },
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The errors of one project's own files: an import of a module the project expects and
|
|
19
|
+
* nothing provides is a setup failure, every other error a type error skipping its unit.
|
|
20
|
+
*/
|
|
21
|
+
function readErrors(project, errors, context) {
|
|
22
|
+
const files = new Map(project.ownSourceFiles().map((file) => [file.fileName, file]));
|
|
23
|
+
const missing = [];
|
|
24
|
+
const skips = [];
|
|
25
|
+
for (const { diagnostic, at } of errors) {
|
|
26
|
+
const file = diagnostic.fileName === undefined ? undefined : files.get(diagnostic.fileName);
|
|
27
|
+
const kind = classOf(diagnostic, true);
|
|
28
|
+
if (file === undefined || kind === "ignored") {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const line = at?.line ?? 1;
|
|
32
|
+
const path = relativePath(context.targetRoot, file.fileName) ?? file.fileName;
|
|
33
|
+
const expected = kind === "module-not-found" ? missingModule(context, file, diagnostic.pos) : undefined;
|
|
34
|
+
if (expected !== undefined) {
|
|
35
|
+
missing.push({
|
|
36
|
+
failureClass: "missing-module",
|
|
37
|
+
detail: `${path}:${String(line)}: ${path} imports ${JSON.stringify(expected.specifier)}, ${expected.reason}`,
|
|
38
|
+
});
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const skip = skipAt(file, diagnostic.pos, line, diagnostic.text, context.targetRoot);
|
|
42
|
+
if (skip !== undefined) {
|
|
43
|
+
skips.push(skip);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { missing, skips };
|
|
47
|
+
}
|
|
48
|
+
function nonEmpty(items) {
|
|
49
|
+
const [first, ...rest] = items;
|
|
50
|
+
return first === undefined ? undefined : [first, ...rest];
|
|
51
|
+
}
|
|
52
|
+
/** One target project read for what keeps it from being analyzed, or the skips it is analyzed with. */
|
|
53
|
+
function classifyTarget(host, run, plan, view, unfound) {
|
|
54
|
+
const { workspace, root } = run;
|
|
55
|
+
const unbuildable = workspace?.unbuildable(plan.configFile);
|
|
56
|
+
if (unbuildable !== undefined) {
|
|
57
|
+
return {
|
|
58
|
+
kind: "setup",
|
|
59
|
+
failures: [{ failureClass: "workspace-member-without-source", detail: unbuildable }],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const referenceOnly = workspace === undefined
|
|
63
|
+
? () => false
|
|
64
|
+
: workspace.referenceOnly(plan.configFile, view.rootFiles);
|
|
65
|
+
const project = workspace === undefined
|
|
66
|
+
? view
|
|
67
|
+
: ownedAs(view, (file, external) => workspace.ownFile(file, external));
|
|
68
|
+
const raw = projectErrors(diagnosticsOf(project));
|
|
69
|
+
const load = nonEmpty(raw.load);
|
|
70
|
+
if (load !== undefined) {
|
|
71
|
+
return { kind: "load", errors: load };
|
|
72
|
+
}
|
|
73
|
+
const reading = readErrors(project, ownErrors(raw.check, referenceOnly), {
|
|
74
|
+
host,
|
|
75
|
+
targetRoot: root,
|
|
76
|
+
paths: pathsOf(plan.configFile, plan.parsed.options),
|
|
77
|
+
member: (manifest) => workspace?.workspace.memberOf(manifest) !== undefined,
|
|
78
|
+
});
|
|
79
|
+
const failures = nonEmpty(reading.missing) ??
|
|
80
|
+
nonEmpty(conventionFailuresFor(run.conventions, plan.configFile, project.ownSourceFiles().map((file) => file.fileName)));
|
|
81
|
+
if (failures !== undefined) {
|
|
82
|
+
return { kind: "setup", failures };
|
|
83
|
+
}
|
|
84
|
+
const display = (path) => relativePath(root, path) ?? path;
|
|
85
|
+
unfound.push(...raw.unfound.map((one) => ({
|
|
86
|
+
configFile: plan.configFile,
|
|
87
|
+
error: renderDiagnostic(one, display),
|
|
88
|
+
})));
|
|
89
|
+
const missingGlobal = raw.unfound.find(({ diagnostic }) => diagnostic.code === MISSING_GLOBAL_TYPE);
|
|
90
|
+
// The checker types every use of a missing global against nothing and places no error in
|
|
91
|
+
// a file, so every file of the project is skipped whole.
|
|
92
|
+
const skips = missingGlobal === undefined
|
|
93
|
+
? reading.skips
|
|
94
|
+
: [
|
|
95
|
+
...reading.skips,
|
|
96
|
+
...project.ownSourceFiles().map((file) => ({
|
|
97
|
+
record: {
|
|
98
|
+
path: display(file.fileName),
|
|
99
|
+
line: 1,
|
|
100
|
+
message: missingGlobal.diagnostic.text,
|
|
101
|
+
},
|
|
102
|
+
file,
|
|
103
|
+
unit: file,
|
|
104
|
+
})),
|
|
105
|
+
];
|
|
106
|
+
return { plan, project, skips, referenceOnly };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* One consumer project read for what keeps its references from being read. A workspace
|
|
110
|
+
* declaration that cannot be read fails it where an import asks whether a package is a member.
|
|
111
|
+
*/
|
|
112
|
+
function classifyConsumer(host, place, plan, consumer, project) {
|
|
113
|
+
const raw = projectErrors(diagnosticsOf(project));
|
|
114
|
+
const load = nonEmpty(raw.load);
|
|
115
|
+
if (load !== undefined) {
|
|
116
|
+
return { kind: "load", errors: load };
|
|
117
|
+
}
|
|
118
|
+
let faulted;
|
|
119
|
+
const reading = readErrors(project, raw.check, {
|
|
120
|
+
host,
|
|
121
|
+
targetRoot: resolvePath(host.workingDirectory(), consumer.path),
|
|
122
|
+
paths: pathsOf(plan.configFile, plan.parsed.options),
|
|
123
|
+
member: (manifest) => {
|
|
124
|
+
if (place !== undefined && "fault" in place) {
|
|
125
|
+
faulted = place.fault;
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
return place?.workspace?.memberOf(manifest) !== undefined;
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
const details = faulted === undefined
|
|
132
|
+
? reading.missing.map((one) => `the consumer ${consumer.path} does not build, its dependencies not installed: ${one.detail}`)
|
|
133
|
+
: [
|
|
134
|
+
`the consumer ${consumer.path} imports a dependency a package below it declares, and its workspace declaration cannot be read to tell whether that package is a member: ${faulted}`,
|
|
135
|
+
];
|
|
136
|
+
const failures = nonEmpty(details.map((detail) => ({ failureClass: "missing-consumer", detail })));
|
|
137
|
+
return failures === undefined ? { project, consumer } : { kind: "setup", failures };
|
|
138
|
+
}
|
|
139
|
+
/** The setup failure one failed configuration ends the run with, its located errors printed first. */
|
|
140
|
+
function ending(plan, failed, root) {
|
|
141
|
+
if (failed.kind === "setup") {
|
|
142
|
+
return failed.failures;
|
|
143
|
+
}
|
|
144
|
+
const what = failed.kind === "load"
|
|
145
|
+
? `the configuration ${relativePath(root, plan.configFile) ?? plan.configFile} does not load, its errors printed above`
|
|
146
|
+
: `the compiler opened no project for ${plan.configFile}`;
|
|
147
|
+
return plan.consumer === undefined
|
|
148
|
+
? [
|
|
149
|
+
{
|
|
150
|
+
failureClass: "no-program",
|
|
151
|
+
detail: `${what}: correct the configuration, or name the configurations to analyze`,
|
|
152
|
+
},
|
|
153
|
+
]
|
|
154
|
+
: [
|
|
155
|
+
{
|
|
156
|
+
failureClass: "missing-consumer",
|
|
157
|
+
detail: `${what}, for the consumer ${plan.consumer.path}: check the consumer out at ${plan.consumer.path} and install its dependencies`,
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
}
|
|
161
|
+
/** The first line of the error that drops one derived configuration. */
|
|
162
|
+
function droppedBy(failed, root) {
|
|
163
|
+
return failed.kind === "load"
|
|
164
|
+
? targetRelative(renderDiagnostic(failed.errors[0], (path) => relativePath(root, path) ?? path), root)
|
|
165
|
+
: failed.kind === "setup"
|
|
166
|
+
? targetRelative(failureLine(failed.failures[0]), root)
|
|
167
|
+
: "the compiler opened no project for the configuration";
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Every opened project of the run classified as analyzed, dropped or failing by the policy
|
|
171
|
+
* table, before any stage reads one. A failure that ends the run is one {@link FailureExit}, by
|
|
172
|
+
* this precedence: a configuration the snapshot opened nothing for, then the setup failures,
|
|
173
|
+
* then the configurations that did not load with their errors, then, where every derived
|
|
174
|
+
* configuration was dropped, their setup failures or the first reason.
|
|
175
|
+
*/
|
|
176
|
+
export function classify(host, run, views) {
|
|
177
|
+
const { root } = run;
|
|
178
|
+
const notBuilt = [...run.discovery.notBuilt];
|
|
179
|
+
const librariesUnfound = [];
|
|
180
|
+
const analyzed = [];
|
|
181
|
+
const consumed = [];
|
|
182
|
+
const ended = new Map();
|
|
183
|
+
const dropped = [];
|
|
184
|
+
const unopened = run.plans.filter((plan) => !views.has(plan.configFile));
|
|
185
|
+
const outcomes = [
|
|
186
|
+
...run.plans.flatMap((plan) => {
|
|
187
|
+
const view = views.get(plan.configFile);
|
|
188
|
+
return view === undefined
|
|
189
|
+
? []
|
|
190
|
+
: [
|
|
191
|
+
{
|
|
192
|
+
plan,
|
|
193
|
+
outcome: plan.consumer === undefined
|
|
194
|
+
? classifyTarget(host, run, plan, view, librariesUnfound)
|
|
195
|
+
: classifyConsumer(host, run.consumerWorkspaces.get(plan.consumer.path), plan, plan.consumer, view),
|
|
196
|
+
},
|
|
197
|
+
];
|
|
198
|
+
}),
|
|
199
|
+
...unopened.map((plan) => ({ plan, outcome: { kind: "unopened" } })),
|
|
200
|
+
];
|
|
201
|
+
for (const { plan, outcome } of outcomes) {
|
|
202
|
+
if (!("kind" in outcome)) {
|
|
203
|
+
if ("consumer" in outcome) {
|
|
204
|
+
consumed.push(outcome);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
analyzed.push(outcome);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else if (POLICY[plan.origin][outcome.kind] === "end") {
|
|
211
|
+
ended.set(outcome.kind, [...(ended.get(outcome.kind) ?? []), { plan, failed: outcome }]);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
dropped.push(outcome);
|
|
215
|
+
notBuilt.push({ id: plan.id, configFile: plan.configFile, error: droppedBy(outcome, root) });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
for (const kind of ["unopened", "setup", "load"]) {
|
|
219
|
+
const all = ended.get(kind) ?? [];
|
|
220
|
+
const failures = nonEmpty(all.flatMap(({ plan, failed }) => ending(plan, failed, root)));
|
|
221
|
+
if (failures !== undefined) {
|
|
222
|
+
throw new FailureExit(kind === "unopened" ? [failures[0]] : failures, all.flatMap(({ failed }) => (failed.kind === "load" ? failed.errors : [])));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const setupDropped = nonEmpty(dropped.flatMap((one) => (one.kind === "setup" ? one.failures : [])));
|
|
226
|
+
const [first] = notBuilt;
|
|
227
|
+
if (analyzed.length === 0 && first !== undefined) {
|
|
228
|
+
throw new FailureExit(setupDropped ?? [
|
|
229
|
+
{
|
|
230
|
+
failureClass: "no-program",
|
|
231
|
+
detail: `no configuration discovery derived could be built; ${first.id}: ${first.error}`,
|
|
232
|
+
},
|
|
233
|
+
]);
|
|
234
|
+
}
|
|
235
|
+
return { analyzed, consumed, notBuilt, librariesUnfound };
|
|
236
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { DiagnosticCategory } from "@typescript/native/sync";
|
|
2
|
+
/** The code of a global type the checker finds in no library the program holds. */
|
|
3
|
+
export const MISSING_GLOBAL_TYPE = 2318;
|
|
4
|
+
/**
|
|
5
|
+
* What each error diagnostic does, first matching row first. In no file: an unfound type library
|
|
6
|
+
* or lib (https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/compiler/processingDiagnostic.go),
|
|
7
|
+
* then the semantic band from 2000; the option band from 5000 leaves the program whole
|
|
8
|
+
* (https://github.com/microsoft/TypeScript/wiki/Coding-guidelines#diagnostic-message-codes).
|
|
9
|
+
* In a file: an unfound module, and where the configuration places the file (5055, 5056, 6059,
|
|
10
|
+
* 6307), which changes no type a statement carries.
|
|
11
|
+
*/
|
|
12
|
+
const DIAGNOSTIC_TABLE = [
|
|
13
|
+
{
|
|
14
|
+
placed: false,
|
|
15
|
+
codes: new Set([MISSING_GLOBAL_TYPE, 2688, 2726, 2727]),
|
|
16
|
+
class: "unfound-library",
|
|
17
|
+
},
|
|
18
|
+
{ placed: false, codes: { from: 2000, below: 3000 }, class: "load" },
|
|
19
|
+
{ placed: true, codes: new Set([2307, 2792]), class: "module-not-found" },
|
|
20
|
+
{ placed: true, codes: new Set([5055, 5056, 6059, 6307]), class: "ignored" },
|
|
21
|
+
];
|
|
22
|
+
/** The category an error diagnostic carries, which the protocol sends as a number. */
|
|
23
|
+
const ERROR_CATEGORY = DiagnosticCategory.Error;
|
|
24
|
+
function isError(diagnostic) {
|
|
25
|
+
return diagnostic.category === ERROR_CATEGORY;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* What one diagnostic does, placed in one of the project's own files or in none. A diagnostic
|
|
29
|
+
* the compiler marks as reporting unnecessary code in a file is no error of the program's.
|
|
30
|
+
*/
|
|
31
|
+
export function classOf(diagnostic, placed) {
|
|
32
|
+
if (!isError(diagnostic) || (placed && diagnostic.reportsUnnecessary === true)) {
|
|
33
|
+
return "ignored";
|
|
34
|
+
}
|
|
35
|
+
const row = DIAGNOSTIC_TABLE.find(({ placed: where, codes }) => where === placed &&
|
|
36
|
+
("has" in codes
|
|
37
|
+
? codes.has(diagnostic.code)
|
|
38
|
+
: diagnostic.code >= codes.from && diagnostic.code < codes.below));
|
|
39
|
+
return row?.class ?? (placed ? "type-error" : "ignored");
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* One diagnostic as `path:line:column: TScode: message`, the position left out where it names
|
|
43
|
+
* none and the file where it names none. `display` spells the file's path.
|
|
44
|
+
*/
|
|
45
|
+
export function renderDiagnostic(located, display = (path) => path) {
|
|
46
|
+
const { diagnostic, at } = located;
|
|
47
|
+
const message = `TS${String(diagnostic.code)}: ${diagnostic.text}`;
|
|
48
|
+
if (diagnostic.fileName === undefined) {
|
|
49
|
+
return message;
|
|
50
|
+
}
|
|
51
|
+
const where = display(diagnostic.fileName);
|
|
52
|
+
return at === undefined
|
|
53
|
+
? `${where}: ${message}`
|
|
54
|
+
: `${where}:${String(at.line)}:${String(at.column)}: ${message}`;
|
|
55
|
+
}
|
|
56
|
+
function locatedIn(diagnostics, locate) {
|
|
57
|
+
return diagnostics.map((diagnostic) => ({
|
|
58
|
+
diagnostic,
|
|
59
|
+
at: diagnostic.fileName === undefined ? undefined : locate(diagnostic.fileName, diagnostic.pos),
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Every error a project's three diagnostic sets carry, in the order the compiler reported
|
|
64
|
+
* them. A resolved configuration carries no diagnostics of its own, so a configuration written
|
|
65
|
+
* with an unreadable option resolves and is refused here, once its project is open.
|
|
66
|
+
*/
|
|
67
|
+
export function diagnosticErrors(sets) {
|
|
68
|
+
return [...sets.configParsing, ...sets.syntactic, ...sets.semantic].filter(isError);
|
|
69
|
+
}
|
|
70
|
+
/** The errors one open project carries, each located in the file it names. */
|
|
71
|
+
export function projectErrors(sets) {
|
|
72
|
+
const unplaced = (kind) => sets.unplaced.filter((diagnostic) => classOf(diagnostic, false) === kind);
|
|
73
|
+
const load = [...sets.configParsing.filter(isError), ...unplaced("load")];
|
|
74
|
+
return {
|
|
75
|
+
load: locatedIn(load, sets.locate),
|
|
76
|
+
check: load.length > 0 ? [] : locatedIn(diagnosticErrors(sets), sets.locate),
|
|
77
|
+
unfound: load.length > 0 ? [] : locatedIn(unplaced("unfound-library"), sets.locate),
|
|
78
|
+
};
|
|
79
|
+
}
|