@cplieger/deadset-ts 8.0.0-dev.8 → 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/src/analysis.js +84 -288
- package/dist/src/configuration-files.js +2 -9
- package/dist/src/consumers.js +1 -28
- package/dist/src/contract/contract.generated.js +20 -10
- package/dist/src/conventions.js +3 -8
- package/dist/src/failure.js +36 -0
- package/dist/src/fs/document.js +21 -28
- package/dist/src/fs/manifest.js +7 -0
- 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/run.js +20 -26
- package/dist/src/scope.js +11 -19
- package/dist/src/session.js +22 -28
- package/dist/src/source-files.js +1 -1
- package/dist/src/type-errors.js +11 -216
- package/dist/src/verbs/analyze.js +90 -109
- package/dist/src/verbs/describe.js +5 -12
- package/dist/src/verbs/version.js +12 -2
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +68 -437
- package/src/configuration-files.ts +8 -10
- package/src/consumers.ts +2 -53
- package/src/contract/contract.generated.ts +20 -10
- package/src/conventions.ts +6 -12
- package/src/failure.ts +63 -0
- package/src/fs/document.ts +23 -27
- package/src/fs/manifest.ts +8 -0
- 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/run.ts +26 -25
- package/src/scope.ts +15 -21
- package/src/session.ts +52 -53
- package/src/source-files.ts +1 -1
- package/src/type-errors.ts +18 -288
- package/src/verbs/analyze.ts +129 -136
- package/src/verbs/describe.ts +4 -11
- 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
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { FAILURE_CLASSES, FAILURE_PREFIXES } from "./contract/contract.generated.js";
|
|
2
|
+
import { PRODUCT } from "./product.js";
|
|
3
|
+
/** The failure as a class line of the error stream, opened by its group's prefix. */
|
|
4
|
+
export function failureLine(failure) {
|
|
5
|
+
return `${FAILURE_PREFIXES[FAILURE_CLASSES[failure.failureClass]]}${failure.failureClass}: ${failure.detail}`;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The lines of the error stream that name the failures: the first one's class line, the run's
|
|
9
|
+
* only one, then each further failure as its class and detail with no group prefix.
|
|
10
|
+
*/
|
|
11
|
+
export function failureLines([first, ...further]) {
|
|
12
|
+
return [failureLine(first), ...further.map((one) => `${one.failureClass}: ${one.detail}`)];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A run the analysis cannot answer. It ends the run with the failure code and no finding
|
|
16
|
+
* list, printing the compiler's located errors and then the failures' lines.
|
|
17
|
+
*/
|
|
18
|
+
export class FailureExit extends Error {
|
|
19
|
+
failures;
|
|
20
|
+
/** The compiler's errors behind the failures, in the order it reported them. */
|
|
21
|
+
diagnostics;
|
|
22
|
+
constructor(failures, diagnostics = []) {
|
|
23
|
+
super(failureLines(failures).join("\n"));
|
|
24
|
+
this.name = "FailureExit";
|
|
25
|
+
this.failures = failures;
|
|
26
|
+
this.diagnostics = diagnostics;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** A failure exit of one failure. */
|
|
30
|
+
export function failureExit(failureClass, detail, diagnostics = []) {
|
|
31
|
+
return new FailureExit([{ failureClass, detail }], diagnostics);
|
|
32
|
+
}
|
|
33
|
+
/** A fault of this analyzer itself, `fault` saying what it cannot do. */
|
|
34
|
+
export function productFault(fault) {
|
|
35
|
+
return failureExit("product-fault", `${PRODUCT} ${fault}: reinstall ${PRODUCT}, and report the fault to its maintainers if a reinstalled ${PRODUCT} fails the same way`);
|
|
36
|
+
}
|
package/dist/src/fs/document.js
CHANGED
|
@@ -5,20 +5,13 @@
|
|
|
5
5
|
* more of it than the Contract's document bound.
|
|
6
6
|
*/
|
|
7
7
|
import { CONFIG_MAX_BYTES } from "../contract/config.generated.js";
|
|
8
|
+
import { failureExit } from "../failure.js";
|
|
8
9
|
import { joinPath, relativePath } from "../paths.js";
|
|
9
10
|
import { message } from "../product.js";
|
|
10
11
|
import { utf8Bytes } from "../reporters/sha256.js";
|
|
11
12
|
import { UsageError } from "../usage.js";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* target root or to anything but a regular file, or the read fails. Nothing is read from
|
|
15
|
-
* it, and the run ends with the failure code.
|
|
16
|
-
*/
|
|
17
|
-
export class TargetDocumentError extends Error {
|
|
18
|
-
constructor(label, reason) {
|
|
19
|
-
super(`${label}: ${reason}`);
|
|
20
|
-
this.name = "TargetDocumentError";
|
|
21
|
-
}
|
|
13
|
+
function unreadable(label, reason) {
|
|
14
|
+
return failureExit("unreadable-document", `${label}: ${reason}: make it a regular file inside the target root that the run can read, or remove it`);
|
|
22
15
|
}
|
|
23
16
|
/** Refuses a document past the Contract's bound with the usage code, naming it `label`. */
|
|
24
17
|
export function refuseOverBound(label, text) {
|
|
@@ -26,34 +19,34 @@ export function refuseOverBound(label, text) {
|
|
|
26
19
|
throw new UsageError(`${label}: holds more than ${String(CONFIG_MAX_BYTES)} bytes`);
|
|
27
20
|
}
|
|
28
21
|
}
|
|
29
|
-
/** What `read` returns, or its throw as the document's {@link TargetDocumentError}. */
|
|
30
|
-
function attempt(label, read) {
|
|
31
|
-
try {
|
|
32
|
-
return read();
|
|
33
|
-
}
|
|
34
|
-
catch (error) {
|
|
35
|
-
throw new TargetDocumentError(label, message(error));
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
22
|
/**
|
|
39
23
|
* The text of the document `name` at the absolute `targetRoot`, undefined where nothing is
|
|
40
|
-
* there
|
|
41
|
-
*
|
|
24
|
+
* there. One whose name resolves outside the target root or to anything but a regular file,
|
|
25
|
+
* or whose read fails, is refused by `refuse` naming it `label`, nothing read from it. A
|
|
26
|
+
* document past the bound is read one byte past it and refused as {@link refuseOverBound} does.
|
|
42
27
|
*/
|
|
43
|
-
export function readTargetDocument(host, targetRoot, name, label = name) {
|
|
28
|
+
export function readTargetDocument(host, targetRoot, name, label = name, refuse = unreadable) {
|
|
29
|
+
const attempt = (read) => {
|
|
30
|
+
try {
|
|
31
|
+
return read();
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
throw refuse(label, message(error));
|
|
35
|
+
}
|
|
36
|
+
};
|
|
44
37
|
const path = joinPath(targetRoot, name);
|
|
45
|
-
const kind = attempt(
|
|
38
|
+
const kind = attempt(() => host.kindOf(path));
|
|
46
39
|
if (kind === "absent") {
|
|
47
40
|
return undefined;
|
|
48
41
|
}
|
|
49
|
-
const real = attempt(
|
|
50
|
-
if (relativePath(attempt(
|
|
51
|
-
throw
|
|
42
|
+
const real = attempt(() => host.realPath(path));
|
|
43
|
+
if (relativePath(attempt(() => host.realPath(targetRoot)), real) === undefined) {
|
|
44
|
+
throw refuse(label, `resolves to ${real}, outside the target root`);
|
|
52
45
|
}
|
|
53
46
|
if (kind !== "file") {
|
|
54
|
-
throw
|
|
47
|
+
throw refuse(label, "is not a regular file");
|
|
55
48
|
}
|
|
56
|
-
const text = attempt(
|
|
49
|
+
const text = attempt(() => host.readFile(real, CONFIG_MAX_BYTES + 1));
|
|
57
50
|
refuseOverBound(label, text);
|
|
58
51
|
return text;
|
|
59
52
|
}
|
package/dist/src/fs/manifest.js
CHANGED
|
@@ -4,6 +4,13 @@ import { joinPath } from "../paths.js";
|
|
|
4
4
|
export const MANIFEST = "package.json";
|
|
5
5
|
/** The directory a package manager installs packages into, whose files are no project's own. */
|
|
6
6
|
export const INSTALLED_PACKAGES = "node_modules";
|
|
7
|
+
/** The manifest members a package declares the packages it depends on under. */
|
|
8
|
+
export const DEPENDENCY_SECTIONS = [
|
|
9
|
+
"dependencies",
|
|
10
|
+
"devDependencies",
|
|
11
|
+
"peerDependencies",
|
|
12
|
+
"optionalDependencies",
|
|
13
|
+
];
|
|
7
14
|
/** Whether one path passes through an installed-packages directory. */
|
|
8
15
|
export function inInstalledPackages(path) {
|
|
9
16
|
return path.split("/").includes(INSTALLED_PACKAGES);
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { LANGUAGE_ROWS } from "../contract/contract.generated.js";
|
|
2
|
+
import { filesBelow } from "../fs/tree.js";
|
|
3
|
+
import { packageScope } from "../inventory.js";
|
|
4
|
+
import { dirnamePath, joinPath, relativePath, resolvePath } from "../paths.js";
|
|
5
|
+
import { LANGUAGE, message } from "../product.js";
|
|
6
|
+
import { globExpression } from "../text/glob.js";
|
|
7
|
+
import { failureExit } from "../failure.js";
|
|
8
|
+
/** The names of the files a program of the language is read from. */
|
|
9
|
+
export const MARKERS = LANGUAGE_ROWS[LANGUAGE].markers;
|
|
10
|
+
const MARKER_EXPRESSIONS = MARKERS.map(globExpression);
|
|
11
|
+
/** Whether one file name is a compiler configuration's: one of the language row's markers. */
|
|
12
|
+
export function isConfigFile(name) {
|
|
13
|
+
return MARKER_EXPRESSIONS.some((marker) => marker.test(name));
|
|
14
|
+
}
|
|
15
|
+
/** Whether the run analyzes one configuration as the target's. */
|
|
16
|
+
export function isTargetConfiguration(configuration) {
|
|
17
|
+
return configuration.origin !== "consumer";
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The first line of one error about the target, every mention of the target root spelled
|
|
21
|
+
* relative to it: a path below the root loses the root and its separator, and the root
|
|
22
|
+
* itself is `.`.
|
|
23
|
+
*/
|
|
24
|
+
export function targetRelative(text, targetRoot) {
|
|
25
|
+
const root = targetRoot.endsWith("/") ? targetRoot.slice(0, -1) : targetRoot;
|
|
26
|
+
const [first = ""] = text.split(/\r?\n/u);
|
|
27
|
+
const spelled = first.split(`${root}/`).join("").split(root).join(".").trim();
|
|
28
|
+
return spelled === "" ? "the configuration could not be built" : spelled;
|
|
29
|
+
}
|
|
30
|
+
/** The configuration files below `root`, by absolute path in ascending order. */
|
|
31
|
+
function configFilesUnder(host, root) {
|
|
32
|
+
return filesBelow(host, root, isConfigFile).map((path) => joinPath(root, path));
|
|
33
|
+
}
|
|
34
|
+
/** The configuration files one parsed configuration's `references` name. */
|
|
35
|
+
function referencesOf(parsed) {
|
|
36
|
+
// The parse answers a directory reference as the directory; the compiler reads a
|
|
37
|
+
// path not ending `.json` as its `tsconfig.json`:
|
|
38
|
+
// https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/core/projectreference.go#L18
|
|
39
|
+
return (parsed.projectReferences ?? []).map(({ path }) => path.endsWith(".json") ? path : joinPath(path, "tsconfig.json"));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The code of a configuration whose `extends` chain returns to a configuration it already
|
|
43
|
+
* passed. A snapshot that opens two configurations whose `extends` chains reach one such
|
|
44
|
+
* cycle panics (`OwnerCache.AddOwner: entry not found`, typescript 7.1.0-dev.20261014.1),
|
|
45
|
+
* so the analysis refuses one before any snapshot is asked to open it.
|
|
46
|
+
*/
|
|
47
|
+
const CIRCULAR_EXTENDS = 18000;
|
|
48
|
+
/** One configuration file's parse, or why it cannot be read. */
|
|
49
|
+
function readConfiguration(engine, configFile) {
|
|
50
|
+
try {
|
|
51
|
+
const parsed = engine.parseConfigFile(configFile);
|
|
52
|
+
const circular = parsed.errors.find(({ code }) => code === CIRCULAR_EXTENDS);
|
|
53
|
+
// The compiler leaves the message's `{0}` unfilled:
|
|
54
|
+
// https://github.com/microsoft/TypeScript/pull/64755
|
|
55
|
+
return circular === undefined
|
|
56
|
+
? { parsed }
|
|
57
|
+
: { reason: `TS${String(circular.code)}: ${circular.text.replace("{0}", () => configFile)}` };
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
return { reason: message(error) };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Every configuration file reached from `pending`, each once, each followed by the
|
|
65
|
+
* configurations it references that `follows` admits, transitively. `unreadable` is told
|
|
66
|
+
* each configuration the compiler cannot read, which the walk then goes on without.
|
|
67
|
+
*/
|
|
68
|
+
function derivedFrom(engine, host, pending, follows, unreadable) {
|
|
69
|
+
const seen = new Set();
|
|
70
|
+
const found = [];
|
|
71
|
+
for (let configFile = pending.shift(); configFile !== undefined; configFile = pending.shift()) {
|
|
72
|
+
const path = resolvePath(host.workingDirectory(), configFile);
|
|
73
|
+
if (seen.has(path)) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
seen.add(path);
|
|
77
|
+
const read = readConfiguration(engine, path);
|
|
78
|
+
if ("reason" in read) {
|
|
79
|
+
unreadable(path, read.reason);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
found.push({ configFile: path, parsed: read.parsed });
|
|
83
|
+
pending.push(...referencesOf(read.parsed).filter(follows));
|
|
84
|
+
}
|
|
85
|
+
return found;
|
|
86
|
+
}
|
|
87
|
+
/** The name one module publishes itself under: the scope's, else its manifest's. */
|
|
88
|
+
export function moduleIdentity(host, module) {
|
|
89
|
+
return module.id === "" ? packageScope(host, module.path)("index.ts").package : module.id;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The compiler configurations one consumer is loaded through: every one under its root and
|
|
93
|
+
* each one those reference, transitively. A consumer that is absent, no directory, holds no
|
|
94
|
+
* configuration or holds one the compiler cannot read ends the run naming it, because a
|
|
95
|
+
* finding computed without a declared consumer would claim a reference set the run never read.
|
|
96
|
+
*/
|
|
97
|
+
function consumerProjects(engine, host, root) {
|
|
98
|
+
const kind = host.kindOf(root);
|
|
99
|
+
if (kind === "absent") {
|
|
100
|
+
throw failureExit("missing-consumer", `the scope declares the consumer ${root}, and nothing is at that path: check the consumer out at ${root} and install its dependencies`);
|
|
101
|
+
}
|
|
102
|
+
if (kind !== "directory") {
|
|
103
|
+
throw failureExit("missing-consumer", `the scope declares the consumer ${root}, which is not a directory: check the consumer out at ${root}`);
|
|
104
|
+
}
|
|
105
|
+
const found = derivedFrom(engine, host, [...configFilesUnder(host, root)], () => true, (configFile, reason) => {
|
|
106
|
+
throw failureExit("missing-consumer", `the consumer ${root} does not load, ${configFile} cannot be read as a compiler configuration: ${reason}`);
|
|
107
|
+
});
|
|
108
|
+
if (found.length === 0) {
|
|
109
|
+
throw failureExit("missing-consumer", `the consumer ${root} holds no compiler configuration, so none of its references can be read: check the consumer out at ${root} with its ${MARKERS.join(", ")}`);
|
|
110
|
+
}
|
|
111
|
+
return found;
|
|
112
|
+
}
|
|
113
|
+
/** A target configuration a matrix or the invocation names that cannot be built, ending the run. */
|
|
114
|
+
function unbuilt(named, reason) {
|
|
115
|
+
throw failureExit("no-program", `${named} ${reason}: correct the configuration, or name the configurations to analyze`);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The projects a build matrix declares, in its order, each entry a file below the target
|
|
119
|
+
* root: an entry naming an absent file, a directory or a file the compiler cannot read ends
|
|
120
|
+
* the run naming it, and one naming an earlier entry's file adds nothing. A reference below
|
|
121
|
+
* the root the matrix does not name ends the run naming both, because the compiler builds a
|
|
122
|
+
* referenced project's sources into the referencing program without reporting their
|
|
123
|
+
* diagnostics. A reference outside the root is an outside project.
|
|
124
|
+
*/
|
|
125
|
+
function declaredProjects(engine, host, targetRoot, entries) {
|
|
126
|
+
const projects = [];
|
|
127
|
+
const references = new Map();
|
|
128
|
+
for (const entry of entries) {
|
|
129
|
+
const configFile = joinPath(targetRoot, entry.project);
|
|
130
|
+
const named = `the project ${JSON.stringify(entry.id)} (${configFile})`;
|
|
131
|
+
const kind = host.kindOf(configFile);
|
|
132
|
+
if (kind !== "file") {
|
|
133
|
+
unbuilt(named, kind === "absent"
|
|
134
|
+
? "does not exist"
|
|
135
|
+
: "is a directory, and a project names its compiler configuration file");
|
|
136
|
+
}
|
|
137
|
+
if (references.has(configFile)) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const read = readConfiguration(engine, configFile);
|
|
141
|
+
if ("reason" in read) {
|
|
142
|
+
unbuilt(named, `cannot be read as a compiler configuration: ${read.reason}`);
|
|
143
|
+
}
|
|
144
|
+
references.set(configFile, referencesOf(read.parsed));
|
|
145
|
+
projects.push({ id: entry.id, configFile, origin: "declared", parsed: read.parsed });
|
|
146
|
+
}
|
|
147
|
+
for (const project of projects) {
|
|
148
|
+
const unnamed = references
|
|
149
|
+
.get(project.configFile)
|
|
150
|
+
?.find((path) => !references.has(path) && relativePath(targetRoot, path) !== undefined);
|
|
151
|
+
if (unnamed !== undefined) {
|
|
152
|
+
unbuilt(`the project ${JSON.stringify(project.id)} (${project.configFile})`, `references ${unnamed}, which the build matrix does not name. A referenced project is built into the program that references it, so the matrix names it too`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return projects;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The projects a run derives where the matrix declares none: the configuration the scope
|
|
159
|
+
* names for the target where its path names one, every compiler configuration under the
|
|
160
|
+
* target root outside a skipped directory, then each configuration below the root those
|
|
161
|
+
* reference, transitively, each once. A configuration the scope names ends the run where it
|
|
162
|
+
* cannot be read; one the walk found is dropped and named.
|
|
163
|
+
*/
|
|
164
|
+
function derivedProjects(engine, host, scope) {
|
|
165
|
+
const targetRoot = scope.target.path;
|
|
166
|
+
const fromScope = scope.target.path.endsWith(".json")
|
|
167
|
+
? [resolvePath(host.workingDirectory(), scope.target.path)]
|
|
168
|
+
: [];
|
|
169
|
+
const absoluteRoot = resolvePath(host.workingDirectory(), targetRoot);
|
|
170
|
+
const rootDir = resolvePath(host.workingDirectory(), fromScope.length > 0 ? dirnamePath(targetRoot) : targetRoot);
|
|
171
|
+
const idOf = (configFile) => relativePath(targetRoot, configFile) ?? configFile;
|
|
172
|
+
const notBuilt = [];
|
|
173
|
+
const found = derivedFrom(engine, host, [...fromScope, ...configFilesUnder(host, targetRoot)], (reference) => relativePath(rootDir, reference) !== undefined, (configFile, reason) => {
|
|
174
|
+
if (fromScope.includes(configFile)) {
|
|
175
|
+
unbuilt(configFile, `cannot be read as a compiler configuration: ${reason}`);
|
|
176
|
+
}
|
|
177
|
+
notBuilt.push({
|
|
178
|
+
id: idOf(configFile),
|
|
179
|
+
configFile,
|
|
180
|
+
error: targetRelative(reason, absoluteRoot),
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
return {
|
|
184
|
+
configurations: found.map(({ configFile, parsed }) => ({
|
|
185
|
+
id: idOf(configFile),
|
|
186
|
+
configFile,
|
|
187
|
+
origin: fromScope.includes(configFile) ? "scope" : "derived",
|
|
188
|
+
parsed,
|
|
189
|
+
})),
|
|
190
|
+
notBuilt,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Every configuration one run opens: the build matrix's project entries in its order, or,
|
|
195
|
+
* where it holds none, those derived from the scope and the target tree, then each declared
|
|
196
|
+
* consumer's, a configuration the target or an earlier consumer opens being theirs alone. A
|
|
197
|
+
* target holding none ends the run with the setup failure `no-program`. No build output is
|
|
198
|
+
* read: a project is the files its configuration names.
|
|
199
|
+
*/
|
|
200
|
+
export function discoverProjects(engine, host, scope, configurations = []) {
|
|
201
|
+
const declared = configurations.filter((entry) => "project" in entry);
|
|
202
|
+
const target = declared.length > 0
|
|
203
|
+
? {
|
|
204
|
+
mode: "declared",
|
|
205
|
+
configurations: declaredProjects(engine, host, scope.target.path, declared),
|
|
206
|
+
notBuilt: [],
|
|
207
|
+
}
|
|
208
|
+
: { mode: "derived", ...derivedProjects(engine, host, scope) };
|
|
209
|
+
if (target.configurations.length === 0 && target.notBuilt.length === 0) {
|
|
210
|
+
throw failureExit("no-program", `${scope.target.path} holds no compiler configuration, no file named ${MARKERS.join(", ")}: run from the directory that holds one, or name the languages to analyze`);
|
|
211
|
+
}
|
|
212
|
+
const opened = new Set(target.configurations.map((one) => one.configFile));
|
|
213
|
+
const consumers = [];
|
|
214
|
+
const consumed = [];
|
|
215
|
+
for (const module of scope.consumers) {
|
|
216
|
+
const read = consumerProjects(engine, host, module.path);
|
|
217
|
+
const consumer = { id: moduleIdentity(host, module), path: module.path };
|
|
218
|
+
consumers.push(consumer);
|
|
219
|
+
for (const { configFile, parsed } of read.filter((one) => !opened.has(one.configFile))) {
|
|
220
|
+
opened.add(configFile);
|
|
221
|
+
consumed.push({ id: configFile, configFile, origin: "consumer", parsed, consumer });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
mode: target.mode,
|
|
226
|
+
configurations: [...target.configurations, ...consumed],
|
|
227
|
+
notBuilt: target.notBuilt,
|
|
228
|
+
consumers,
|
|
229
|
+
};
|
|
230
|
+
}
|