@cplieger/deadset-ts 2.2.1 → 3.0.0
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/README.md +9 -16
- package/dist/src/analysis.js +142 -0
- package/dist/src/components.js +289 -0
- package/dist/src/config.js +6 -0
- package/dist/src/discover.js +98 -24
- package/dist/src/entry-point-gaps.js +278 -0
- package/dist/src/entry-points.js +385 -0
- package/dist/src/finding.js +12 -0
- package/dist/src/glob.js +65 -0
- package/dist/src/graph.js +116 -0
- package/dist/src/index.js +1 -2
- package/dist/src/manifest.js +120 -0
- package/dist/src/print.js +3 -6
- package/dist/src/references.js +156 -106
- package/dist/src/resolve.js +82 -20
- package/dist/src/roots.js +345 -0
- package/dist/src/run.js +59 -10
- package/dist/src/schema.js +1 -1
- package/dist/src/session.js +6 -1
- package/dist/src/source-files.js +122 -0
- package/dist/src/sweep.js +156 -0
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +3 -3
- package/src/analysis.ts +236 -0
- package/src/components.ts +372 -0
- package/src/config.ts +30 -2
- package/src/discover.ts +144 -29
- package/src/entry-point-gaps.ts +304 -0
- package/src/entry-points.ts +496 -0
- package/src/finding.ts +43 -0
- package/src/glob.ts +67 -0
- package/src/graph.ts +178 -0
- package/src/index.ts +1 -20
- package/src/manifest.ts +174 -0
- package/src/print.ts +5 -6
- package/src/references.ts +201 -111
- package/src/resolve.ts +117 -24
- package/src/roots.ts +467 -0
- package/src/run.ts +66 -11
- package/src/schema.ts +5 -2
- package/src/session.ts +6 -1
- package/src/source-files.ts +157 -0
- package/src/sweep.ts +230 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The root set of one project: every declaration the analysis keeps live without a
|
|
3
|
+
* reference, and why.
|
|
4
|
+
*
|
|
5
|
+
* A root makes its declaration live under REACHABILITY and plays no part in
|
|
6
|
+
* reference counting, which asks only whether some declaration of the loaded graph
|
|
7
|
+
* names it. The two relations are different sets, so an exported declaration that
|
|
8
|
+
* something outside the program reaches is live under the first and still a
|
|
9
|
+
* candidate under the second.
|
|
10
|
+
*/
|
|
11
|
+
import { REPOSITORY_DOCUMENT } from "./config.js";
|
|
12
|
+
import { entryPoints } from "./entry-points.js";
|
|
13
|
+
import { globExpression } from "./glob.js";
|
|
14
|
+
import { nodeKey } from "./inventory.js";
|
|
15
|
+
import { byPosition } from "./position.js";
|
|
16
|
+
import { sourceFilesOf } from "./source-files.js";
|
|
17
|
+
/** The code the Contract gives a configured root that matches nothing. */
|
|
18
|
+
const UNMATCHED_ROOT_CODE = "DS1704";
|
|
19
|
+
/** The subject kind a finding under that code carries, from the Contract's vocabulary. */
|
|
20
|
+
const UNMATCHED_ROOT_SUBJECT = "root";
|
|
21
|
+
/** The fixed position of a finding about a document rather than about a line of one. */
|
|
22
|
+
const DOCUMENT_POSITION = 1;
|
|
23
|
+
/** Whether a configured string carries either special character. */
|
|
24
|
+
function isPattern(text) {
|
|
25
|
+
return text.includes("*") || text.includes("?");
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Whether ref matches a pattern whose only special characters are `*`, which stands
|
|
29
|
+
* for any run of characters including the solidus, and `?`, which stands for exactly
|
|
30
|
+
* one character counted as a code point. A star keeps the position it last stood at,
|
|
31
|
+
* so the shortest run it can stand for is tried first.
|
|
32
|
+
*/
|
|
33
|
+
function matchGlob(pattern, ref) {
|
|
34
|
+
let next = 0;
|
|
35
|
+
let star = -1;
|
|
36
|
+
let resume = 0;
|
|
37
|
+
let at = 0;
|
|
38
|
+
while (at < ref.length) {
|
|
39
|
+
if (next < pattern.length && (pattern[next] === "?" || pattern[next] === ref[at])) {
|
|
40
|
+
next += 1;
|
|
41
|
+
at += 1;
|
|
42
|
+
}
|
|
43
|
+
else if (next < pattern.length && pattern[next] === "*") {
|
|
44
|
+
star = next;
|
|
45
|
+
resume = at;
|
|
46
|
+
next += 1;
|
|
47
|
+
}
|
|
48
|
+
else if (star >= 0) {
|
|
49
|
+
resume += 1;
|
|
50
|
+
next = star + 1;
|
|
51
|
+
at = resume;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
while (next < pattern.length && pattern[next] === "*") {
|
|
58
|
+
next += 1;
|
|
59
|
+
}
|
|
60
|
+
return next === pattern.length;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Whether one configured string names the symbol reference ref. A string carrying
|
|
64
|
+
* neither special character names only the reference it spells.
|
|
65
|
+
*
|
|
66
|
+
* Both sides are read as code points: a character outside the basic plane is two
|
|
67
|
+
* string units, and `?` stands for one character.
|
|
68
|
+
*/
|
|
69
|
+
export function matchRef(pattern, ref) {
|
|
70
|
+
if (!isPattern(pattern)) {
|
|
71
|
+
return pattern === ref;
|
|
72
|
+
}
|
|
73
|
+
return matchGlob(Array.from(pattern), Array.from(ref));
|
|
74
|
+
}
|
|
75
|
+
/** Two strings ordered bytewise, which is the order every set of a run is read in. */
|
|
76
|
+
function compare(a, b) {
|
|
77
|
+
if (a === b) {
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
return a < b ? -1 : 1;
|
|
81
|
+
}
|
|
82
|
+
/** The members of one declaration, indexed by the container that holds them. */
|
|
83
|
+
function childrenOf(held) {
|
|
84
|
+
const byParent = new Map();
|
|
85
|
+
for (const symbol of held.symbols) {
|
|
86
|
+
const siblings = byParent.get(symbol.parent);
|
|
87
|
+
if (siblings === undefined) {
|
|
88
|
+
byParent.set(symbol.parent, [symbol]);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
siblings.push(symbol);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return byParent;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The roots one project holds, and every configured string that named nothing in it.
|
|
98
|
+
*
|
|
99
|
+
* A file a manifest member, an entry-file pattern or a tool's configuration names
|
|
100
|
+
* is a root with everything its module exports: each name of the module's export
|
|
101
|
+
* table, and where a name is a re-export, every link of the chain to the declaration
|
|
102
|
+
* it carries, so a re-exported declaration is a root of the file that publishes it.
|
|
103
|
+
* A file a runtime executes without reading its exports, a test file or a worker, is
|
|
104
|
+
* a root by itself.
|
|
105
|
+
*/
|
|
106
|
+
export function roots(project, held, targetRoot, options) {
|
|
107
|
+
const files = sourceFilesOf(project, targetRoot);
|
|
108
|
+
const declared = held.declarations;
|
|
109
|
+
const fileIds = new Map();
|
|
110
|
+
for (const file of files.byName.values()) {
|
|
111
|
+
const id = declared.get(nodeKey(file, file));
|
|
112
|
+
if (id !== undefined) {
|
|
113
|
+
fileIds.set(file.fileName, id);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const found = new Map();
|
|
117
|
+
const add = (id, kind, source) => {
|
|
118
|
+
found.set([id, kind, source].join("\u0000"), { id, kind, source });
|
|
119
|
+
};
|
|
120
|
+
const entered = [];
|
|
121
|
+
const enter = (file, kind, source, exports) => {
|
|
122
|
+
entered.push({ file, kind, source, exports });
|
|
123
|
+
};
|
|
124
|
+
const published = [];
|
|
125
|
+
const publishes = (entry) => entry.published &&
|
|
126
|
+
(options.manifest.declaresExports ? entry.member.startsWith("exports") : true);
|
|
127
|
+
for (const entry of options.manifest.entries) {
|
|
128
|
+
for (const file of files.named(entry.path)) {
|
|
129
|
+
enter(file, entry.role === "run" ? "manifest-binary" : "manifest-entry", entry.member, true);
|
|
130
|
+
if (options.publishedAPI && publishes(entry)) {
|
|
131
|
+
published.push(file);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const pattern of options.entryFiles) {
|
|
136
|
+
const expression = globExpression(pattern);
|
|
137
|
+
for (const [path, file] of files.byPath) {
|
|
138
|
+
if (expression.test(path)) {
|
|
139
|
+
enter(file, "entry-file", pattern, true);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (const point of entryPoints(project, files, options.testFiles)) {
|
|
144
|
+
enter(point.file, point.rule, point.source, point.exports);
|
|
145
|
+
}
|
|
146
|
+
const exportsOf = exportTables(project, held, [...entered.filter((entry) => entry.exports).map((entry) => entry.file), ...published], files.byName);
|
|
147
|
+
for (const entry of entered) {
|
|
148
|
+
const id = fileIds.get(entry.file.fileName);
|
|
149
|
+
if (id === undefined) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
add(id, entry.kind, entry.source);
|
|
153
|
+
if (entry.exports) {
|
|
154
|
+
for (const exported of exportsOf.get(entry.file.fileName) ?? []) {
|
|
155
|
+
add(exported, entry.kind, entry.source);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (options.publishedAPI) {
|
|
160
|
+
for (const id of publishedAPI(published, exportsOf, held)) {
|
|
161
|
+
add(id, "published-api", "");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const unmatched = configured(held, options.patterns, add);
|
|
165
|
+
const sites = new Map(held.symbols.map((symbol) => [symbol.id, symbol.position]));
|
|
166
|
+
const ordered = [...found.values()].sort((a, b) => {
|
|
167
|
+
const left = sites.get(a.id);
|
|
168
|
+
const right = sites.get(b.id);
|
|
169
|
+
const site = left === undefined || right === undefined ? 0 : byPosition(left, right);
|
|
170
|
+
return site || compare(a.kind, b.kind) || compare(a.source, b.source);
|
|
171
|
+
});
|
|
172
|
+
return { configFile: project.configFile, liveUnderReachability: ordered, unmatched };
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Every declaration each file's module exports, by the file's name: the declaration
|
|
176
|
+
* each name of the export table is, and every link of a re-export's chain. A name
|
|
177
|
+
* whose declaration is outside the project's own files belongs to another program
|
|
178
|
+
* and names nothing here.
|
|
179
|
+
*
|
|
180
|
+
* One batch resolves every file to its module and one request per module reads
|
|
181
|
+
* its export table, star re-exports resolved into it. A file that is no module
|
|
182
|
+
* exports nothing.
|
|
183
|
+
*/
|
|
184
|
+
function exportTables(project, held, wanted, ownFiles) {
|
|
185
|
+
const unique = [...new Map(wanted.map((file) => [file.fileName, file])).values()];
|
|
186
|
+
const tables = new Map();
|
|
187
|
+
if (unique.length === 0) {
|
|
188
|
+
return tables;
|
|
189
|
+
}
|
|
190
|
+
const modules = project.symbolsAt(unique.map((file) => project.handle(file)));
|
|
191
|
+
const chains = new Map();
|
|
192
|
+
const chainOf = (symbol) => {
|
|
193
|
+
const cached = chains.get(symbol.id);
|
|
194
|
+
if (cached !== undefined) {
|
|
195
|
+
return cached;
|
|
196
|
+
}
|
|
197
|
+
const ids = [];
|
|
198
|
+
const walked = new Set();
|
|
199
|
+
let at = symbol;
|
|
200
|
+
while (at !== undefined && !walked.has(at.id)) {
|
|
201
|
+
walked.add(at.id);
|
|
202
|
+
for (const handle of at.declarations) {
|
|
203
|
+
if (!ownFiles.has(handle.path)) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const node = project.declarationAt(handle)?.node;
|
|
207
|
+
const id = node === undefined
|
|
208
|
+
? undefined
|
|
209
|
+
: held.declarations.get(nodeKey(node.getSourceFile(), node));
|
|
210
|
+
if (id !== undefined && !ids.includes(id)) {
|
|
211
|
+
ids.push(id);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
at = project.aliasStepOf(at);
|
|
215
|
+
}
|
|
216
|
+
chains.set(symbol.id, ids);
|
|
217
|
+
return ids;
|
|
218
|
+
};
|
|
219
|
+
unique.forEach((file, index) => {
|
|
220
|
+
const module = modules[index];
|
|
221
|
+
if (module === undefined) {
|
|
222
|
+
tables.set(file.fileName, []);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const ids = new Set();
|
|
226
|
+
for (const exported of project.checker.getExportsOfModule(module)) {
|
|
227
|
+
for (const id of chainOf(exported)) {
|
|
228
|
+
ids.add(id);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
tables.set(file.fileName, [...ids]);
|
|
232
|
+
});
|
|
233
|
+
return tables;
|
|
234
|
+
}
|
|
235
|
+
/** The kinds of container whose export table, not its members' visibility, says what it publishes. */
|
|
236
|
+
const EXPORTING_CONTAINERS = new Set(["file", "namespace"]);
|
|
237
|
+
/**
|
|
238
|
+
* Every declaration a consumer of the library can name: what the published files
|
|
239
|
+
* export, and below each, the members a consumer reaches through it.
|
|
240
|
+
*
|
|
241
|
+
* The published files are the ones `exports` reaches where the manifest declares
|
|
242
|
+
* one, because a package that declares `exports` has said exactly what a consumer may
|
|
243
|
+
* import; where it declares none, they are the ones the members that preceded it
|
|
244
|
+
* name. Below a published module or namespace the walk follows what its export table
|
|
245
|
+
* names; below any other published declaration it follows every member a consumer
|
|
246
|
+
* can write: a public one, and a protected one, which a consumer's subclass names. A
|
|
247
|
+
* private member is not published, nor is anything below one, and a type parameter
|
|
248
|
+
* is not published either, because a consumer supplies a type argument by position.
|
|
249
|
+
*
|
|
250
|
+
* A published file is not itself published. A consumer imports a module and names
|
|
251
|
+
* what it exports; the file is a root because the manifest names it, which is a
|
|
252
|
+
* different reason.
|
|
253
|
+
*/
|
|
254
|
+
function publishedAPI(published, exportsOf, held) {
|
|
255
|
+
const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
|
|
256
|
+
const children = childrenOf(held);
|
|
257
|
+
const reached = new Set();
|
|
258
|
+
const pending = [];
|
|
259
|
+
for (const file of published) {
|
|
260
|
+
for (const id of exportsOf.get(file.fileName) ?? []) {
|
|
261
|
+
if (byId.get(id)?.kind === "file") {
|
|
262
|
+
pending.push(id);
|
|
263
|
+
}
|
|
264
|
+
else if (!reached.has(id)) {
|
|
265
|
+
reached.add(id);
|
|
266
|
+
pending.push(id);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
|
|
271
|
+
const exporting = EXPORTING_CONTAINERS.has(byId.get(next)?.kind ?? "");
|
|
272
|
+
for (const member of children.get(next) ?? []) {
|
|
273
|
+
const reachable = exporting
|
|
274
|
+
? member.exported
|
|
275
|
+
: member.visibility !== "private" && member.visibility !== "private-name";
|
|
276
|
+
if (member.kind === "type-parameter" || reached.has(member.id) || !reachable) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
reached.add(member.id);
|
|
280
|
+
pending.push(member.id);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return [...reached];
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Keeps every declaration the configuration names and returns every string that
|
|
287
|
+
* named none, in configuration order. A string the configuration lists twice is one
|
|
288
|
+
* configured root.
|
|
289
|
+
*/
|
|
290
|
+
function configured(held, patterns, add) {
|
|
291
|
+
const unmatched = [];
|
|
292
|
+
for (const pattern of new Set(patterns)) {
|
|
293
|
+
const kind = isPattern(pattern) ? "pattern" : "configured";
|
|
294
|
+
let matched = false;
|
|
295
|
+
for (const symbol of held.symbols) {
|
|
296
|
+
if (matchRef(pattern, symbol.ref)) {
|
|
297
|
+
add(symbol.id, kind, pattern);
|
|
298
|
+
matched = true;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (!matched) {
|
|
302
|
+
unmatched.push(pattern);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return unmatched;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Every configured string that named no declaration in ANY project of the run, in
|
|
309
|
+
* configuration order and each once.
|
|
310
|
+
*
|
|
311
|
+
* A finding about a configured root is about the configuration and not about a
|
|
312
|
+
* declaration, so there is one per run: a string that names a declaration of one
|
|
313
|
+
* project has named something, and the projects it named nothing in are not a
|
|
314
|
+
* finding. A run with no project names nothing with any of them.
|
|
315
|
+
*/
|
|
316
|
+
export function unmatchedEverywhere(patterns, perProject) {
|
|
317
|
+
return [...new Set(patterns)].filter((pattern) => perProject.every((project) => project.unmatched.includes(pattern)));
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* The findings a run reports for the configured strings that named nothing.
|
|
321
|
+
*
|
|
322
|
+
* `document` is the path of the document that declared the roots, below the target
|
|
323
|
+
* root; where the run read them from no document of the target it is empty and the
|
|
324
|
+
* finding names the conventional repository configuration, which is the document a
|
|
325
|
+
* maintainer writes a configured root in. Every finding of one run sits at that
|
|
326
|
+
* document's first position: the roots are written as an array whose members carry
|
|
327
|
+
* no line of their own, so the string each finding names is what tells one from
|
|
328
|
+
* another.
|
|
329
|
+
*/
|
|
330
|
+
export function unmatchedRoots(sources, document) {
|
|
331
|
+
const position = {
|
|
332
|
+
path: document === "" ? REPOSITORY_DOCUMENT : document,
|
|
333
|
+
line: DOCUMENT_POSITION,
|
|
334
|
+
column: DOCUMENT_POSITION,
|
|
335
|
+
endLine: DOCUMENT_POSITION,
|
|
336
|
+
};
|
|
337
|
+
return sources.map((source) => ({
|
|
338
|
+
code: UNMATCHED_ROOT_CODE,
|
|
339
|
+
position,
|
|
340
|
+
symbol: { ref: source, kind: UNMATCHED_ROOT_SUBJECT, name: source, sizeLines: 1 },
|
|
341
|
+
message: isPattern(source)
|
|
342
|
+
? "configured root pattern matches no symbol of the inventory"
|
|
343
|
+
: "configured root matches no symbol of the inventory",
|
|
344
|
+
}));
|
|
345
|
+
}
|
package/dist/src/run.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { runRoots } from "./analysis.js";
|
|
2
|
+
import { ConfigError, REPOSITORY_DOCUMENT } from "./config.js";
|
|
2
3
|
import { diagnosticErrors, discoverProjects, DiscoveryError, renderDiagnostic, } from "./discover.js";
|
|
3
4
|
import { joinPath, resolvePath } from "./paths.js";
|
|
4
5
|
import { printConfig } from "./print.js";
|
|
5
|
-
import { resolve } from "./resolve.js";
|
|
6
|
+
import { resolve, resolveMatrix } from "./resolve.js";
|
|
6
7
|
import { readScope, ScopeError, scopeForDir } from "./scope.js";
|
|
7
8
|
import { diagnosticsOf, openEngine, runSession } from "./session.js";
|
|
8
9
|
import { CONTRACT_VERSION } from "./version.js";
|
|
9
|
-
/** The repository configuration's name at the target root. */
|
|
10
|
-
const REPOSITORY_DOCUMENT = "deadset.json";
|
|
11
10
|
/** The exit codes the Contract's exit-code table names. */
|
|
12
11
|
const EXIT_CLEAN = 0;
|
|
12
|
+
const EXIT_FINDINGS = 1;
|
|
13
13
|
const EXIT_USAGE = 2;
|
|
14
14
|
const EXIT_FAILURE = 3;
|
|
15
15
|
const VERBS = [
|
|
@@ -171,6 +171,9 @@ export function run(args, out, err, host, openClient = (collectTiming) => openEn
|
|
|
171
171
|
if (verb === "print-projects") {
|
|
172
172
|
return printProjectsVerb(args.slice(1), out, err, host, openClient);
|
|
173
173
|
}
|
|
174
|
+
if (verb === "print-roots") {
|
|
175
|
+
return printRootsVerb(args.slice(1), out, err, host, openClient);
|
|
176
|
+
}
|
|
174
177
|
if (verb !== undefined && VERBS.includes(verb)) {
|
|
175
178
|
err.write(`deadset-ts: ${verb} is not implemented\n`);
|
|
176
179
|
return EXIT_USAGE;
|
|
@@ -194,21 +197,25 @@ function printConfigVerb(args, out, err, host) {
|
|
|
194
197
|
}
|
|
195
198
|
}
|
|
196
199
|
/**
|
|
197
|
-
* Writes the
|
|
198
|
-
*
|
|
200
|
+
* Writes the name of every project one run analyzes, one per line, having read each
|
|
201
|
+
* project's diagnostics first: the identifier the build matrix declares for it, or,
|
|
202
|
+
* where discovery derived the project, its configuration file's path below the
|
|
203
|
+
* target root.
|
|
199
204
|
*/
|
|
200
205
|
function printProjectsVerb(args, out, err, host, openClient) {
|
|
201
206
|
let engine;
|
|
202
207
|
try {
|
|
203
208
|
const options = readOptions(args);
|
|
209
|
+
const matrix = resolveMatrix(inputsOf(host, options));
|
|
204
210
|
const scope = scopeOf(host, options);
|
|
205
211
|
engine = openClient(false);
|
|
206
|
-
const discovered = discoverProjects(engine, host, scope);
|
|
212
|
+
const discovered = discoverProjects(engine, host, scope, matrix);
|
|
213
|
+
const ids = new Map(discovered.projects.map((project) => [project.configFile, project.id]));
|
|
207
214
|
const failures = [];
|
|
208
215
|
const { projects } = runSession(engine, discovered.configFiles, (project) => {
|
|
209
216
|
const errors = diagnosticErrors(diagnosticsOf(project));
|
|
210
217
|
failures.push(...errors.map(renderDiagnostic));
|
|
211
|
-
return project.configFile;
|
|
218
|
+
return ids.get(project.configFile) ?? project.configFile;
|
|
212
219
|
});
|
|
213
220
|
if (failures.length > 0) {
|
|
214
221
|
for (const line of failures) {
|
|
@@ -217,8 +224,8 @@ function printProjectsVerb(args, out, err, host, openClient) {
|
|
|
217
224
|
err.write(`deadset-ts: ${String(failures.length)} error(s); no answer was produced\n`);
|
|
218
225
|
return EXIT_FAILURE;
|
|
219
226
|
}
|
|
220
|
-
for (const
|
|
221
|
-
out.write(`${
|
|
227
|
+
for (const id of projects) {
|
|
228
|
+
out.write(`${id}\n`);
|
|
222
229
|
}
|
|
223
230
|
return EXIT_CLEAN;
|
|
224
231
|
}
|
|
@@ -227,6 +234,48 @@ function printProjectsVerb(args, out, err, host, openClient) {
|
|
|
227
234
|
return refuse(error, err);
|
|
228
235
|
}
|
|
229
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* One root as a line: the declaration's reference, why it is a root, and the string
|
|
239
|
+
* that named it, separated by tabs. A run of several configurations adds the ones
|
|
240
|
+
* whose project holds the root as a fourth field, and writes the third present and
|
|
241
|
+
* empty where no string named the root, so every line of one run holds the same
|
|
242
|
+
* fields.
|
|
243
|
+
*/
|
|
244
|
+
function rootLine(root, several) {
|
|
245
|
+
const line = `${root.ref}\t${root.kind}`;
|
|
246
|
+
if (several) {
|
|
247
|
+
return `${line}\t${root.source}\t${root.configurations.join(" ")}`;
|
|
248
|
+
}
|
|
249
|
+
return root.source === "" ? line : `${line}\t${root.source}`;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Writes the root set of the run, one root per line, and then names on the error
|
|
253
|
+
* stream, by its issue kind, every configured root or pattern that named nothing;
|
|
254
|
+
* one such string fails the run. The line shape is this command's own rather than a
|
|
255
|
+
* Contract format, so that sort and cut read it.
|
|
256
|
+
*/
|
|
257
|
+
function printRootsVerb(args, out, err, host, openClient) {
|
|
258
|
+
let engine;
|
|
259
|
+
try {
|
|
260
|
+
const options = readOptions(args);
|
|
261
|
+
const { config, provenance } = resolve(inputsOf(host, options));
|
|
262
|
+
const scope = scopeOf(host, options);
|
|
263
|
+
engine = openClient(false);
|
|
264
|
+
const answer = runRoots(engine, host, scope, config, provenance);
|
|
265
|
+
const several = answer.configurations.length > 1;
|
|
266
|
+
for (const root of answer.roots) {
|
|
267
|
+
out.write(`${rootLine(root, several)}\n`);
|
|
268
|
+
}
|
|
269
|
+
for (const finding of answer.findings) {
|
|
270
|
+
err.write(`${finding.code}: roots.patterns names nothing: ${finding.symbol.ref}\n`);
|
|
271
|
+
}
|
|
272
|
+
return answer.findings.length > 0 ? EXIT_FINDINGS : EXIT_CLEAN;
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
engine?.close();
|
|
276
|
+
return refuse(error, err);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
230
279
|
/** Turns one refusal into the exit code the Contract's table gives it. */
|
|
231
280
|
function refuse(error, err) {
|
|
232
281
|
if (error instanceof ConfigError) {
|
package/dist/src/schema.js
CHANGED
|
@@ -19,7 +19,7 @@ export const SCHEMA_ROOT = {
|
|
|
19
19
|
consumer_tests: leaf,
|
|
20
20
|
configurations: {
|
|
21
21
|
kind: "list",
|
|
22
|
-
members: { id: leaf, os: leaf, arch: leaf, tags: leaf },
|
|
22
|
+
members: { id: leaf, os: leaf, arch: leaf, tags: leaf, project: leaf },
|
|
23
23
|
},
|
|
24
24
|
matrix: { kind: "section", members: { complete: leaf } },
|
|
25
25
|
template_dirs: leaf,
|
package/dist/src/session.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { API } from "@typescript/native/unstable/sync";
|
|
1
|
+
import { API, SymbolFlags } from "@typescript/native/unstable/sync";
|
|
2
2
|
import { isIdentifier } from "@typescript/native/unstable/ast";
|
|
3
3
|
/**
|
|
4
4
|
* Opens the compiler client the analysis reads through.
|
|
@@ -53,6 +53,11 @@ function viewOf(project) {
|
|
|
53
53
|
resolvedSymbolAt: ({ node }) => isIdentifier(node) ? project.checker.getResolvedSymbol(node) : undefined,
|
|
54
54
|
shorthandValueAt: ({ node }) => project.checker.getShorthandAssignmentValueSymbol(node),
|
|
55
55
|
aliasStepOf: (symbol) => {
|
|
56
|
+
// The step is asked of an alias only: asked of any other symbol, the compiler
|
|
57
|
+
// fails an assertion and the session ends.
|
|
58
|
+
if ((symbol.flags & SymbolFlags.Alias) === 0) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
56
61
|
const held = project.checker.getImmediateAliasedSymbol(symbol);
|
|
57
62
|
// An alias that resolves to nothing answers with the checker's own unknown
|
|
58
63
|
// symbol, which declares nothing; a symbol that is no alias answers with
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The target's own source files of one project, looked up the ways a path written
|
|
3
|
+
* in a document or a string names one: as the file itself, or as a file the
|
|
4
|
+
* compiler emits from it.
|
|
5
|
+
*/
|
|
6
|
+
import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.js";
|
|
7
|
+
/** The source extensions the compiler emits each output extension from. */
|
|
8
|
+
const EMITTED = [
|
|
9
|
+
{ emitted: ".d.ts", sources: [".ts", ".tsx"] },
|
|
10
|
+
{ emitted: ".d.mts", sources: [".mts"] },
|
|
11
|
+
{ emitted: ".d.cts", sources: [".cts"] },
|
|
12
|
+
{ emitted: ".js", sources: [".ts", ".tsx"] },
|
|
13
|
+
{ emitted: ".jsx", sources: [".tsx"] },
|
|
14
|
+
{ emitted: ".mjs", sources: [".mts"] },
|
|
15
|
+
{ emitted: ".cjs", sources: [".cts"] },
|
|
16
|
+
];
|
|
17
|
+
/** The one character a subpath pattern of a manifest's `exports` holds. */
|
|
18
|
+
const SUBPATH_WILDCARD = "*";
|
|
19
|
+
/** One path with its extension replaced, where it carries the one named. */
|
|
20
|
+
function reExtended(path, emitted, source) {
|
|
21
|
+
return path.endsWith(emitted) ? path.slice(0, path.length - emitted.length) + source : undefined;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The longest directory every path shares, which is the directory the compiler
|
|
25
|
+
* emits from where the configuration declares no root directory.
|
|
26
|
+
*/
|
|
27
|
+
function commonDirectory(paths) {
|
|
28
|
+
const first = paths[0];
|
|
29
|
+
if (first === undefined) {
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
let common = dirnamePath(first).split("/");
|
|
33
|
+
for (const path of paths.slice(1)) {
|
|
34
|
+
const segments = dirnamePath(path).split("/");
|
|
35
|
+
let shared = 0;
|
|
36
|
+
while (shared < common.length &&
|
|
37
|
+
shared < segments.length &&
|
|
38
|
+
common[shared] === segments[shared]) {
|
|
39
|
+
shared += 1;
|
|
40
|
+
}
|
|
41
|
+
common = common.slice(0, shared);
|
|
42
|
+
}
|
|
43
|
+
return common.join("/");
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Whether one path is what a subpath pattern stands for: the text before the
|
|
47
|
+
* wildcard, any run of characters including the solidus, then the text after it.
|
|
48
|
+
*/
|
|
49
|
+
function matchesSubpath(pattern, path) {
|
|
50
|
+
const at = pattern.indexOf(SUBPATH_WILDCARD);
|
|
51
|
+
const before = pattern.slice(0, at);
|
|
52
|
+
const after = pattern.slice(at + SUBPATH_WILDCARD.length);
|
|
53
|
+
return (path.length >= before.length + after.length && path.startsWith(before) && path.endsWith(after));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The lookups over one project's own files.
|
|
57
|
+
*
|
|
58
|
+
* A path that names an emitted file is read back to the source the project would
|
|
59
|
+
* emit it from through the project's own emit mapping and nothing else: the
|
|
60
|
+
* extension is one the compiler emits from a source extension, and a path below the
|
|
61
|
+
* output directory is read below the directory the compiler emits from, which the
|
|
62
|
+
* configuration declares or which is the directory every input file shares.
|
|
63
|
+
*/
|
|
64
|
+
export function sourceFilesOf(project, targetRoot) {
|
|
65
|
+
const byName = new Map();
|
|
66
|
+
const byPath = new Map();
|
|
67
|
+
for (const file of project.ownSourceFiles()) {
|
|
68
|
+
byName.set(file.fileName, file);
|
|
69
|
+
const path = relativePath(targetRoot, file.fileName);
|
|
70
|
+
if (path !== undefined) {
|
|
71
|
+
byPath.set(path, file);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const options = project.program.getCompilerOptions();
|
|
75
|
+
const configDir = dirnamePath(project.configFile);
|
|
76
|
+
const at = (option) => option === undefined ? undefined : resolvePath(configDir, option);
|
|
77
|
+
const outDir = at(options.outDir);
|
|
78
|
+
const declarationDir = at(options.declarationDir);
|
|
79
|
+
const sourceDir = at(options.rootDir) ?? commonDirectory([...byName.keys()]);
|
|
80
|
+
/** One path read below the directory the compiler emits from, where it is below the one it emits to. */
|
|
81
|
+
const fromEmit = (path, emitted) => {
|
|
82
|
+
const emitDir = emitted.startsWith(".d.") ? (declarationDir ?? outDir) : outDir;
|
|
83
|
+
if (emitDir === undefined) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
const below = relativePath(emitDir, path);
|
|
87
|
+
return below === undefined ? undefined : joinPath(sourceDir, below);
|
|
88
|
+
};
|
|
89
|
+
/** The source paths one written path may stand for, the path itself first. */
|
|
90
|
+
const candidates = (path) => {
|
|
91
|
+
const found = [path];
|
|
92
|
+
for (const { emitted, sources } of EMITTED) {
|
|
93
|
+
const mapped = fromEmit(path, emitted);
|
|
94
|
+
for (const source of sources) {
|
|
95
|
+
for (const from of [path, mapped]) {
|
|
96
|
+
const candidate = from === undefined ? undefined : reExtended(from, emitted, source);
|
|
97
|
+
if (candidate !== undefined) {
|
|
98
|
+
found.push(candidate);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return found;
|
|
104
|
+
};
|
|
105
|
+
return {
|
|
106
|
+
byPath,
|
|
107
|
+
byName,
|
|
108
|
+
named: (path) => {
|
|
109
|
+
if (!path.includes(SUBPATH_WILDCARD)) {
|
|
110
|
+
for (const candidate of candidates(path)) {
|
|
111
|
+
const held = byName.get(candidate);
|
|
112
|
+
if (held !== undefined) {
|
|
113
|
+
return [held];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
const patterns = candidates(path).filter((candidate) => candidate.includes(SUBPATH_WILDCARD));
|
|
119
|
+
return [...byName.values()].filter((file) => patterns.some((pattern) => matchesSubpath(pattern, file.fileName)));
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|