@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.
Files changed (137) hide show
  1. package/dist/bin/node-host.js +21 -3
  2. package/dist/src/analysis.js +94 -299
  3. package/dist/src/config.js +6 -83
  4. package/dist/src/configuration-files.js +2 -9
  5. package/dist/src/configured-declarations.js +15 -20
  6. package/dist/src/consumers.js +3 -30
  7. package/dist/src/contract/config.generated.js +544 -0
  8. package/dist/src/contract/contract.generated.js +20 -10
  9. package/dist/src/contract/report.generated.js +3 -2
  10. package/dist/src/conventions.js +3 -8
  11. package/dist/src/edges.js +36 -112
  12. package/dist/src/exempt-classes.js +0 -2
  13. package/dist/src/exempt.js +1 -1
  14. package/dist/src/failure.js +36 -0
  15. package/dist/src/findings/completion.js +5 -4
  16. package/dist/src/findings/emitters.js +1 -1
  17. package/dist/src/findings/intra-function.js +1 -1
  18. package/dist/src/findings/non-code-artifacts.js +2 -2
  19. package/dist/src/findings/unused-declarations.js +1 -1
  20. package/dist/src/findings/visibility-narrowing.js +2 -2
  21. package/dist/src/findings-pass.js +1 -1
  22. package/dist/src/framework-lifecycle.js +7 -7
  23. package/dist/src/fs/document.js +52 -0
  24. package/dist/src/fs/manifest.js +7 -0
  25. package/dist/src/injection-container.js +1 -1
  26. package/dist/src/json-document.js +194 -67
  27. package/dist/src/kinds.js +0 -7
  28. package/dist/src/load/classify.js +236 -0
  29. package/dist/src/load/diagnostics.js +79 -0
  30. package/dist/src/load/discover.js +230 -0
  31. package/dist/src/{emit-map.js → load/emit-map.js} +1 -1
  32. package/dist/src/load/expected-module.js +136 -0
  33. package/dist/src/load/load.js +22 -0
  34. package/dist/src/load/module-lookup.js +37 -0
  35. package/dist/src/load/plan.js +155 -0
  36. package/dist/src/{test-globals.js → load/test-globals.js} +51 -56
  37. package/dist/src/load/test-joins.js +82 -0
  38. package/dist/src/{workspace-manifests.js → load/workspace-manifests.js} +3 -3
  39. package/dist/src/{workspace-programs.js → load/workspace-programs.js} +37 -40
  40. package/dist/src/{workspace-resolution.js → load/workspace-resolution.js} +21 -59
  41. package/dist/src/{workspace-run.js → load/workspace-run.js} +4 -3
  42. package/dist/src/{workspace.js → load/workspace.js} +21 -17
  43. package/dist/src/outside-tests.js +1 -1
  44. package/dist/src/print.js +10 -79
  45. package/dist/src/report-schema.js +3 -224
  46. package/dist/src/resolve.js +245 -715
  47. package/dist/src/run.js +59 -72
  48. package/dist/src/schema.js +320 -344
  49. package/dist/src/scope.js +37 -113
  50. package/dist/src/serialization-contract.js +1 -1
  51. package/dist/src/session.js +22 -28
  52. package/dist/src/source-files.js +1 -1
  53. package/dist/src/suppress-file.js +15 -17
  54. package/dist/src/suppress.js +3 -3
  55. package/dist/src/template-field.js +4 -4
  56. package/dist/src/type-errors.js +11 -216
  57. package/dist/src/usage.js +10 -0
  58. package/dist/src/verbs/analyze.js +100 -122
  59. package/dist/src/verbs/describe.js +5 -12
  60. package/dist/src/verbs/explain.js +3 -3
  61. package/dist/src/verbs/print-projects.js +2 -2
  62. package/dist/src/verbs/version.js +12 -2
  63. package/jsr.json +1 -1
  64. package/package.json +1 -1
  65. package/src/analysis.ts +95 -460
  66. package/src/config.ts +31 -298
  67. package/src/configuration-files.ts +8 -10
  68. package/src/configured-declarations.ts +18 -23
  69. package/src/consumers.ts +6 -57
  70. package/src/contract/config.generated.ts +798 -0
  71. package/src/contract/contract.generated.ts +20 -10
  72. package/src/contract/report.generated.ts +3 -2
  73. package/src/conventions.ts +6 -12
  74. package/src/edges.ts +39 -124
  75. package/src/exempt-classes.ts +0 -5
  76. package/src/exempt.ts +4 -4
  77. package/src/failure.ts +63 -0
  78. package/src/findings/completion.ts +5 -6
  79. package/src/findings/emitter.ts +2 -2
  80. package/src/findings/emitters.ts +1 -1
  81. package/src/findings/intra-function.ts +1 -1
  82. package/src/findings/non-code-artifacts.ts +4 -4
  83. package/src/findings/unused-declarations.ts +1 -1
  84. package/src/findings/visibility-narrowing.ts +2 -2
  85. package/src/findings-pass.ts +3 -3
  86. package/src/framework-lifecycle.ts +7 -7
  87. package/src/fs/document.ts +70 -0
  88. package/src/fs/manifest.ts +8 -0
  89. package/src/host.ts +12 -5
  90. package/src/injection-container.ts +1 -1
  91. package/src/json-document.ts +236 -76
  92. package/src/kinds.ts +0 -8
  93. package/src/load/classify.ts +352 -0
  94. package/src/load/diagnostics.ts +141 -0
  95. package/src/load/discover.ts +378 -0
  96. package/src/{emit-map.ts → load/emit-map.ts} +1 -1
  97. package/src/load/expected-module.ts +188 -0
  98. package/src/load/load.ts +61 -0
  99. package/src/load/module-lookup.ts +50 -0
  100. package/src/load/plan.ts +258 -0
  101. package/src/{test-globals.ts → load/test-globals.ts} +59 -65
  102. package/src/load/test-joins.ts +98 -0
  103. package/src/{workspace-manifests.ts → load/workspace-manifests.ts} +4 -4
  104. package/src/{workspace-programs.ts → load/workspace-programs.ts} +59 -53
  105. package/src/{workspace-resolution.ts → load/workspace-resolution.ts} +31 -78
  106. package/src/{workspace-run.ts → load/workspace-run.ts} +14 -11
  107. package/src/{workspace.ts → load/workspace.ts} +28 -21
  108. package/src/outside-tests.ts +1 -1
  109. package/src/partial.ts +1 -1
  110. package/src/print.ts +11 -92
  111. package/src/report-schema.ts +3 -299
  112. package/src/resolve.ts +281 -1105
  113. package/src/run.ts +68 -80
  114. package/src/schema.ts +391 -389
  115. package/src/scope.ts +45 -131
  116. package/src/serialization-contract.ts +1 -1
  117. package/src/session.ts +52 -53
  118. package/src/source-files.ts +1 -1
  119. package/src/suppress-file.ts +15 -20
  120. package/src/suppress.ts +3 -3
  121. package/src/template-field.ts +10 -7
  122. package/src/type-errors.ts +18 -288
  123. package/src/usage.ts +10 -0
  124. package/src/verbs/analyze.ts +139 -157
  125. package/src/verbs/describe.ts +4 -11
  126. package/src/verbs/explain.ts +3 -7
  127. package/src/verbs/print-projects.ts +2 -2
  128. package/src/verbs/verb.ts +2 -2
  129. package/src/verbs/version.ts +13 -2
  130. package/dist/src/base-url.js +0 -18
  131. package/dist/src/discover.js +0 -374
  132. package/dist/src/setup-failure.js +0 -19
  133. package/dist/src/test-joins.js +0 -101
  134. package/src/base-url.ts +0 -24
  135. package/src/discover.ts +0 -548
  136. package/src/setup-failure.ts +0 -37
  137. package/src/test-joins.ts +0 -118
@@ -1,106 +1,228 @@
1
1
  /**
2
- * A read of a strict JSON document beside its parse: what a parse into a value cannot
3
- * report, the offset of each record of one array and a member written twice.
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
- /** The token walk over a document `JSON.parse` accepted, so every token is well formed. */
6
- class Walk {
7
- starts = [];
8
- /** The member written twice, where one is, as the offset of its name. */
9
- twice;
10
- text;
11
- array;
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
- constructor(text, array) {
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.array = array;
85
+ this.records = records;
16
86
  }
17
- run() {
18
- this.value("document");
19
- }
20
- skip() {
21
- while (/[ \t\r\n]/u.test(this.text[this.at] ?? "")) {
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
- string() {
26
- const start = this.at;
27
- this.at += 1;
28
- while (this.text[this.at] !== '"') {
29
- this.at += this.text[this.at] === "\\" ? 2 : 1;
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(role) {
35
- this.skip();
36
- if (role === "record") {
37
- this.starts.push(this.at);
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(role);
118
+ return this.object(depth);
42
119
  }
43
- else if (char === "[") {
44
- this.list(role === "array" ? "record" : "other");
120
+ if (char === "[") {
121
+ return this.array(depth);
45
122
  }
46
- else if (char === '"') {
47
- this.string();
123
+ const string = this.match(STRING);
124
+ if (string !== undefined) {
125
+ return JSON.parse(string);
48
126
  }
49
- else {
50
- while (this.at < this.text.length && !/[,\]} \t\r\n]/u.test(this.text[this.at] ?? "")) {
51
- this.at += 1;
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(role) {
56
- const seen = new Set();
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.skip();
60
- if (this.text[this.at] === "}") {
61
- this.at += 1;
62
- return;
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
- if (this.text[this.at] === ",") {
65
- this.at += 1;
66
- this.skip();
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
- const offset = this.at;
69
- const name = this.string();
70
- if (seen.has(name) && this.twice === undefined) {
71
- this.twice = { name, offset };
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
- seen.add(name);
74
- this.skip();
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
- this.value(role === "document" && name === this.array ? "array" : "other");
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
- list(element) {
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.skip();
83
- if (this.text[this.at] === "]") {
84
- this.at += 1;
85
- return;
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 (this.text[this.at] === ",") {
88
- this.at += 1;
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
- * Walks a document `JSON.parse` accepted, whose top-level member `array` holds the
96
- * records. A member one object writes twice reads as though it were written once, its
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 walkDocument(text, array) {
101
- const walk = new Walk(text, array);
102
- walk.run();
103
- return { starts: walk.starts, twice: walk.twice };
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
+ }