@cplieger/deadset-ts 2.2.1 → 2.3.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 +5 -3
- package/dist/src/analysis.js +109 -0
- package/dist/src/config.js +6 -0
- 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/index.js +1 -0
- package/dist/src/manifest.js +120 -0
- package/dist/src/references.js +2 -57
- package/dist/src/roots.js +345 -0
- package/dist/src/run.js +48 -3
- package/dist/src/session.js +6 -1
- package/dist/src/source-files.js +122 -0
- package/jsr.json +1 -1
- package/package.json +3 -3
- package/src/analysis.ts +169 -0
- package/src/config.ts +7 -0
- 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/index.ts +1 -0
- package/src/manifest.ts +174 -0
- package/src/references.ts +2 -59
- package/src/roots.ts +467 -0
- package/src/run.ts +55 -4
- package/src/session.ts +6 -1
- package/src/source-files.ts +157 -0
package/src/roots.ts
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
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
|
+
|
|
12
|
+
import type { SourceFile } from "@typescript/native/unstable/ast";
|
|
13
|
+
import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
|
|
14
|
+
import { REPOSITORY_DOCUMENT } from "./config.ts";
|
|
15
|
+
import { entryPoints, type EntryRule } from "./entry-points.ts";
|
|
16
|
+
import type { Finding } from "./finding.ts";
|
|
17
|
+
import { globExpression } from "./glob.ts";
|
|
18
|
+
import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
|
|
19
|
+
import type { Manifest, ManifestEntry } from "./manifest.ts";
|
|
20
|
+
import { byPosition } from "./position.ts";
|
|
21
|
+
import type { ProjectView } from "./session.ts";
|
|
22
|
+
import { sourceFilesOf } from "./source-files.ts";
|
|
23
|
+
|
|
24
|
+
/** Why one declaration is a root. */
|
|
25
|
+
export type RootKind =
|
|
26
|
+
/** A file `ts.entry_files` names, and what it exports. */
|
|
27
|
+
| "entry-file"
|
|
28
|
+
/** A file the manifest names as an importable entry point, and what it exports. */
|
|
29
|
+
| "manifest-entry"
|
|
30
|
+
/** A file the manifest names as a command, and what it exports. */
|
|
31
|
+
| "manifest-binary"
|
|
32
|
+
/** A declaration a consumer of a library target can name. */
|
|
33
|
+
| "published-api"
|
|
34
|
+
/** A declaration an exact reference from the configuration names. */
|
|
35
|
+
| "configured"
|
|
36
|
+
/** A declaration a pattern from the configuration names. */
|
|
37
|
+
| "pattern"
|
|
38
|
+
/** A file a tool or the platform enters by its own convention, and what it exports where the tool reads it. */
|
|
39
|
+
| EntryRule;
|
|
40
|
+
|
|
41
|
+
/** One declaration the analysis keeps live under reachability, and why. */
|
|
42
|
+
export interface Root {
|
|
43
|
+
/** The identifier of the declaration, as the inventory spells it. */
|
|
44
|
+
readonly id: string;
|
|
45
|
+
readonly kind: RootKind;
|
|
46
|
+
/**
|
|
47
|
+
* The string from a document that named this root: the configured root or pattern,
|
|
48
|
+
* the entry-file or test-file pattern that matched, the manifest member that named
|
|
49
|
+
* the file, the file-name convention a tool looks for, or the literal a
|
|
50
|
+
* configuration or a call writes. It is empty where a rule alone decided the root.
|
|
51
|
+
*/
|
|
52
|
+
readonly source: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** One project's roots, and every configured string that named nothing in it. */
|
|
56
|
+
export interface Roots {
|
|
57
|
+
/** The compiler configuration the project was opened from. */
|
|
58
|
+
readonly configFile: string;
|
|
59
|
+
/**
|
|
60
|
+
* Every root of this project, ordered by the site of the declaration it names,
|
|
61
|
+
* then by kind, then by the string that named it. A declaration that is a root for
|
|
62
|
+
* more than one reason appears once per reason, so an explanation can name every
|
|
63
|
+
* reason it is live.
|
|
64
|
+
*
|
|
65
|
+
* The member says which relation the set feeds: every declaration here is live
|
|
66
|
+
* under reachability, and nothing here is live under reference counting.
|
|
67
|
+
*/
|
|
68
|
+
readonly liveUnderReachability: readonly Root[];
|
|
69
|
+
/**
|
|
70
|
+
* Every configured root and pattern that named no declaration of this project, in
|
|
71
|
+
* configuration order and each once. It is per project, and a finding is per run:
|
|
72
|
+
* {@link unmatchedEverywhere} is what turns one into the other.
|
|
73
|
+
*/
|
|
74
|
+
readonly unmatched: readonly string[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** What decides the roots beside the declarations themselves. */
|
|
78
|
+
export interface RootOptions {
|
|
79
|
+
/** The target's own manifest. */
|
|
80
|
+
readonly manifest: Manifest;
|
|
81
|
+
/** `roots.patterns` from the resolved configuration. */
|
|
82
|
+
readonly patterns: readonly string[];
|
|
83
|
+
/** `ts.entry_files` from the resolved configuration. */
|
|
84
|
+
readonly entryFiles: readonly string[];
|
|
85
|
+
/** `ts.test_files` from the resolved configuration. */
|
|
86
|
+
readonly testFiles: readonly string[];
|
|
87
|
+
/** Whether the target is a library, so a consumer outside it reaches its published API. */
|
|
88
|
+
readonly publishedAPI: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The code the Contract gives a configured root that matches nothing. */
|
|
92
|
+
const UNMATCHED_ROOT_CODE = "DS1704";
|
|
93
|
+
|
|
94
|
+
/** The subject kind a finding under that code carries, from the Contract's vocabulary. */
|
|
95
|
+
const UNMATCHED_ROOT_SUBJECT = "root";
|
|
96
|
+
|
|
97
|
+
/** The fixed position of a finding about a document rather than about a line of one. */
|
|
98
|
+
const DOCUMENT_POSITION = 1;
|
|
99
|
+
|
|
100
|
+
/** Whether a configured string carries either special character. */
|
|
101
|
+
function isPattern(text: string): boolean {
|
|
102
|
+
return text.includes("*") || text.includes("?");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Whether ref matches a pattern whose only special characters are `*`, which stands
|
|
107
|
+
* for any run of characters including the solidus, and `?`, which stands for exactly
|
|
108
|
+
* one character counted as a code point. A star keeps the position it last stood at,
|
|
109
|
+
* so the shortest run it can stand for is tried first.
|
|
110
|
+
*/
|
|
111
|
+
function matchGlob(pattern: readonly string[], ref: readonly string[]): boolean {
|
|
112
|
+
let next = 0;
|
|
113
|
+
let star = -1;
|
|
114
|
+
let resume = 0;
|
|
115
|
+
let at = 0;
|
|
116
|
+
while (at < ref.length) {
|
|
117
|
+
if (next < pattern.length && (pattern[next] === "?" || pattern[next] === ref[at])) {
|
|
118
|
+
next += 1;
|
|
119
|
+
at += 1;
|
|
120
|
+
} else if (next < pattern.length && pattern[next] === "*") {
|
|
121
|
+
star = next;
|
|
122
|
+
resume = at;
|
|
123
|
+
next += 1;
|
|
124
|
+
} else if (star >= 0) {
|
|
125
|
+
resume += 1;
|
|
126
|
+
next = star + 1;
|
|
127
|
+
at = resume;
|
|
128
|
+
} else {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
while (next < pattern.length && pattern[next] === "*") {
|
|
133
|
+
next += 1;
|
|
134
|
+
}
|
|
135
|
+
return next === pattern.length;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Whether one configured string names the symbol reference ref. A string carrying
|
|
140
|
+
* neither special character names only the reference it spells.
|
|
141
|
+
*
|
|
142
|
+
* Both sides are read as code points: a character outside the basic plane is two
|
|
143
|
+
* string units, and `?` stands for one character.
|
|
144
|
+
*/
|
|
145
|
+
export function matchRef(pattern: string, ref: string): boolean {
|
|
146
|
+
if (!isPattern(pattern)) {
|
|
147
|
+
return pattern === ref;
|
|
148
|
+
}
|
|
149
|
+
return matchGlob(Array.from(pattern), Array.from(ref));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Two strings ordered bytewise, which is the order every set of a run is read in. */
|
|
153
|
+
function compare(a: string, b: string): number {
|
|
154
|
+
if (a === b) {
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
return a < b ? -1 : 1;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The members of one declaration, indexed by the container that holds them. */
|
|
161
|
+
function childrenOf(held: Inventory): ReadonlyMap<string, readonly InventorySymbol[]> {
|
|
162
|
+
const byParent = new Map<string, InventorySymbol[]>();
|
|
163
|
+
for (const symbol of held.symbols) {
|
|
164
|
+
const siblings = byParent.get(symbol.parent);
|
|
165
|
+
if (siblings === undefined) {
|
|
166
|
+
byParent.set(symbol.parent, [symbol]);
|
|
167
|
+
} else {
|
|
168
|
+
siblings.push(symbol);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return byParent;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The roots one project holds, and every configured string that named nothing in it.
|
|
176
|
+
*
|
|
177
|
+
* A file a manifest member, an entry-file pattern or a tool's configuration names
|
|
178
|
+
* is a root with everything its module exports: each name of the module's export
|
|
179
|
+
* table, and where a name is a re-export, every link of the chain to the declaration
|
|
180
|
+
* it carries, so a re-exported declaration is a root of the file that publishes it.
|
|
181
|
+
* A file a runtime executes without reading its exports, a test file or a worker, is
|
|
182
|
+
* a root by itself.
|
|
183
|
+
*/
|
|
184
|
+
export function roots<Brand>(
|
|
185
|
+
project: ProjectView<Brand>,
|
|
186
|
+
held: Inventory,
|
|
187
|
+
targetRoot: string,
|
|
188
|
+
options: RootOptions,
|
|
189
|
+
): Roots {
|
|
190
|
+
const files = sourceFilesOf(project, targetRoot);
|
|
191
|
+
const declared = held.declarations;
|
|
192
|
+
const fileIds = new Map<string, string>();
|
|
193
|
+
for (const file of files.byName.values()) {
|
|
194
|
+
const id = declared.get(nodeKey(file, file));
|
|
195
|
+
if (id !== undefined) {
|
|
196
|
+
fileIds.set(file.fileName, id);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const found = new Map<string, Root>();
|
|
200
|
+
const add = (id: string, kind: RootKind, source: string): void => {
|
|
201
|
+
found.set([id, kind, source].join("\u0000"), { id, kind, source });
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const entered: { file: SourceFile; kind: RootKind; source: string; exports: boolean }[] = [];
|
|
205
|
+
const enter = (file: SourceFile, kind: RootKind, source: string, exports: boolean): void => {
|
|
206
|
+
entered.push({ file, kind, source, exports });
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const published: SourceFile[] = [];
|
|
210
|
+
const publishes = (entry: ManifestEntry): boolean =>
|
|
211
|
+
entry.published &&
|
|
212
|
+
(options.manifest.declaresExports ? entry.member.startsWith("exports") : true);
|
|
213
|
+
for (const entry of options.manifest.entries) {
|
|
214
|
+
for (const file of files.named(entry.path)) {
|
|
215
|
+
enter(file, entry.role === "run" ? "manifest-binary" : "manifest-entry", entry.member, true);
|
|
216
|
+
if (options.publishedAPI && publishes(entry)) {
|
|
217
|
+
published.push(file);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (const pattern of options.entryFiles) {
|
|
222
|
+
const expression = globExpression(pattern);
|
|
223
|
+
for (const [path, file] of files.byPath) {
|
|
224
|
+
if (expression.test(path)) {
|
|
225
|
+
enter(file, "entry-file", pattern, true);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
for (const point of entryPoints(project, files, options.testFiles)) {
|
|
230
|
+
enter(point.file, point.rule, point.source, point.exports);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const exportsOf = exportTables(
|
|
234
|
+
project,
|
|
235
|
+
held,
|
|
236
|
+
[...entered.filter((entry) => entry.exports).map((entry) => entry.file), ...published],
|
|
237
|
+
files.byName,
|
|
238
|
+
);
|
|
239
|
+
for (const entry of entered) {
|
|
240
|
+
const id = fileIds.get(entry.file.fileName);
|
|
241
|
+
if (id === undefined) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
add(id, entry.kind, entry.source);
|
|
245
|
+
if (entry.exports) {
|
|
246
|
+
for (const exported of exportsOf.get(entry.file.fileName) ?? []) {
|
|
247
|
+
add(exported, entry.kind, entry.source);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (options.publishedAPI) {
|
|
253
|
+
for (const id of publishedAPI(published, exportsOf, held)) {
|
|
254
|
+
add(id, "published-api", "");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const unmatched = configured(held, options.patterns, add);
|
|
259
|
+
|
|
260
|
+
const sites = new Map(held.symbols.map((symbol) => [symbol.id, symbol.position]));
|
|
261
|
+
const ordered = [...found.values()].sort((a, b) => {
|
|
262
|
+
const left = sites.get(a.id);
|
|
263
|
+
const right = sites.get(b.id);
|
|
264
|
+
const site = left === undefined || right === undefined ? 0 : byPosition(left, right);
|
|
265
|
+
return site || compare(a.kind, b.kind) || compare(a.source, b.source);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
return { configFile: project.configFile, liveUnderReachability: ordered, unmatched };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Every declaration each file's module exports, by the file's name: the declaration
|
|
273
|
+
* each name of the export table is, and every link of a re-export's chain. A name
|
|
274
|
+
* whose declaration is outside the project's own files belongs to another program
|
|
275
|
+
* and names nothing here.
|
|
276
|
+
*
|
|
277
|
+
* One batch resolves every file to its module and one request per module reads
|
|
278
|
+
* its export table, star re-exports resolved into it. A file that is no module
|
|
279
|
+
* exports nothing.
|
|
280
|
+
*/
|
|
281
|
+
function exportTables<Brand>(
|
|
282
|
+
project: ProjectView<Brand>,
|
|
283
|
+
held: Inventory,
|
|
284
|
+
wanted: readonly SourceFile[],
|
|
285
|
+
ownFiles: ReadonlyMap<string, SourceFile>,
|
|
286
|
+
): ReadonlyMap<string, readonly string[]> {
|
|
287
|
+
const unique = [...new Map(wanted.map((file) => [file.fileName, file])).values()];
|
|
288
|
+
const tables = new Map<string, readonly string[]>();
|
|
289
|
+
if (unique.length === 0) {
|
|
290
|
+
return tables;
|
|
291
|
+
}
|
|
292
|
+
const modules = project.symbolsAt(unique.map((file) => project.handle(file)));
|
|
293
|
+
const chains = new Map<number, readonly string[]>();
|
|
294
|
+
|
|
295
|
+
const chainOf = (symbol: TSSymbol): readonly string[] => {
|
|
296
|
+
const cached = chains.get(symbol.id);
|
|
297
|
+
if (cached !== undefined) {
|
|
298
|
+
return cached;
|
|
299
|
+
}
|
|
300
|
+
const ids: string[] = [];
|
|
301
|
+
const walked = new Set<number>();
|
|
302
|
+
let at: TSSymbol | undefined = symbol;
|
|
303
|
+
while (at !== undefined && !walked.has(at.id)) {
|
|
304
|
+
walked.add(at.id);
|
|
305
|
+
for (const handle of at.declarations) {
|
|
306
|
+
if (!ownFiles.has(handle.path)) {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const node = project.declarationAt(handle)?.node;
|
|
310
|
+
const id =
|
|
311
|
+
node === undefined
|
|
312
|
+
? undefined
|
|
313
|
+
: held.declarations.get(nodeKey(node.getSourceFile(), node));
|
|
314
|
+
if (id !== undefined && !ids.includes(id)) {
|
|
315
|
+
ids.push(id);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
at = project.aliasStepOf(at);
|
|
319
|
+
}
|
|
320
|
+
chains.set(symbol.id, ids);
|
|
321
|
+
return ids;
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
unique.forEach((file, index) => {
|
|
325
|
+
const module = modules[index];
|
|
326
|
+
if (module === undefined) {
|
|
327
|
+
tables.set(file.fileName, []);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const ids = new Set<string>();
|
|
331
|
+
for (const exported of project.checker.getExportsOfModule(module)) {
|
|
332
|
+
for (const id of chainOf(exported)) {
|
|
333
|
+
ids.add(id);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
tables.set(file.fileName, [...ids]);
|
|
337
|
+
});
|
|
338
|
+
return tables;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** The kinds of container whose export table, not its members' visibility, says what it publishes. */
|
|
342
|
+
const EXPORTING_CONTAINERS: ReadonlySet<string> = new Set(["file", "namespace"]);
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Every declaration a consumer of the library can name: what the published files
|
|
346
|
+
* export, and below each, the members a consumer reaches through it.
|
|
347
|
+
*
|
|
348
|
+
* The published files are the ones `exports` reaches where the manifest declares
|
|
349
|
+
* one, because a package that declares `exports` has said exactly what a consumer may
|
|
350
|
+
* import; where it declares none, they are the ones the members that preceded it
|
|
351
|
+
* name. Below a published module or namespace the walk follows what its export table
|
|
352
|
+
* names; below any other published declaration it follows every member a consumer
|
|
353
|
+
* can write: a public one, and a protected one, which a consumer's subclass names. A
|
|
354
|
+
* private member is not published, nor is anything below one, and a type parameter
|
|
355
|
+
* is not published either, because a consumer supplies a type argument by position.
|
|
356
|
+
*
|
|
357
|
+
* A published file is not itself published. A consumer imports a module and names
|
|
358
|
+
* what it exports; the file is a root because the manifest names it, which is a
|
|
359
|
+
* different reason.
|
|
360
|
+
*/
|
|
361
|
+
function publishedAPI(
|
|
362
|
+
published: readonly SourceFile[],
|
|
363
|
+
exportsOf: ReadonlyMap<string, readonly string[]>,
|
|
364
|
+
held: Inventory,
|
|
365
|
+
): readonly string[] {
|
|
366
|
+
const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
|
|
367
|
+
const children = childrenOf(held);
|
|
368
|
+
const reached = new Set<string>();
|
|
369
|
+
const pending: string[] = [];
|
|
370
|
+
for (const file of published) {
|
|
371
|
+
for (const id of exportsOf.get(file.fileName) ?? []) {
|
|
372
|
+
if (byId.get(id)?.kind === "file") {
|
|
373
|
+
pending.push(id);
|
|
374
|
+
} else if (!reached.has(id)) {
|
|
375
|
+
reached.add(id);
|
|
376
|
+
pending.push(id);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
|
|
381
|
+
const exporting = EXPORTING_CONTAINERS.has(byId.get(next)?.kind ?? "");
|
|
382
|
+
for (const member of children.get(next) ?? []) {
|
|
383
|
+
const reachable = exporting
|
|
384
|
+
? member.exported
|
|
385
|
+
: member.visibility !== "private" && member.visibility !== "private-name";
|
|
386
|
+
if (member.kind === "type-parameter" || reached.has(member.id) || !reachable) {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
reached.add(member.id);
|
|
390
|
+
pending.push(member.id);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return [...reached];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Keeps every declaration the configuration names and returns every string that
|
|
398
|
+
* named none, in configuration order. A string the configuration lists twice is one
|
|
399
|
+
* configured root.
|
|
400
|
+
*/
|
|
401
|
+
function configured(
|
|
402
|
+
held: Inventory,
|
|
403
|
+
patterns: readonly string[],
|
|
404
|
+
add: (id: string, kind: RootKind, source: string) => void,
|
|
405
|
+
): readonly string[] {
|
|
406
|
+
const unmatched: string[] = [];
|
|
407
|
+
for (const pattern of new Set(patterns)) {
|
|
408
|
+
const kind: RootKind = isPattern(pattern) ? "pattern" : "configured";
|
|
409
|
+
let matched = false;
|
|
410
|
+
for (const symbol of held.symbols) {
|
|
411
|
+
if (matchRef(pattern, symbol.ref)) {
|
|
412
|
+
add(symbol.id, kind, pattern);
|
|
413
|
+
matched = true;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (!matched) {
|
|
417
|
+
unmatched.push(pattern);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return unmatched;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Every configured string that named no declaration in ANY project of the run, in
|
|
425
|
+
* configuration order and each once.
|
|
426
|
+
*
|
|
427
|
+
* A finding about a configured root is about the configuration and not about a
|
|
428
|
+
* declaration, so there is one per run: a string that names a declaration of one
|
|
429
|
+
* project has named something, and the projects it named nothing in are not a
|
|
430
|
+
* finding. A run with no project names nothing with any of them.
|
|
431
|
+
*/
|
|
432
|
+
export function unmatchedEverywhere(
|
|
433
|
+
patterns: readonly string[],
|
|
434
|
+
perProject: readonly Roots[],
|
|
435
|
+
): readonly string[] {
|
|
436
|
+
return [...new Set(patterns)].filter((pattern) =>
|
|
437
|
+
perProject.every((project) => project.unmatched.includes(pattern)),
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* The findings a run reports for the configured strings that named nothing.
|
|
443
|
+
*
|
|
444
|
+
* `document` is the path of the document that declared the roots, below the target
|
|
445
|
+
* root; where the run read them from no document of the target it is empty and the
|
|
446
|
+
* finding names the conventional repository configuration, which is the document a
|
|
447
|
+
* maintainer writes a configured root in. Every finding of one run sits at that
|
|
448
|
+
* document's first position: the roots are written as an array whose members carry
|
|
449
|
+
* no line of their own, so the string each finding names is what tells one from
|
|
450
|
+
* another.
|
|
451
|
+
*/
|
|
452
|
+
export function unmatchedRoots(sources: readonly string[], document: string): readonly Finding[] {
|
|
453
|
+
const position = {
|
|
454
|
+
path: document === "" ? REPOSITORY_DOCUMENT : document,
|
|
455
|
+
line: DOCUMENT_POSITION,
|
|
456
|
+
column: DOCUMENT_POSITION,
|
|
457
|
+
endLine: DOCUMENT_POSITION,
|
|
458
|
+
};
|
|
459
|
+
return sources.map((source) => ({
|
|
460
|
+
code: UNMATCHED_ROOT_CODE,
|
|
461
|
+
position,
|
|
462
|
+
symbol: { ref: source, kind: UNMATCHED_ROOT_SUBJECT, name: source, sizeLines: 1 },
|
|
463
|
+
message: isPattern(source)
|
|
464
|
+
? "configured root pattern matches no symbol of the inventory"
|
|
465
|
+
: "configured root matches no symbol of the inventory",
|
|
466
|
+
}));
|
|
467
|
+
}
|
package/src/run.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { runRoots, type RunRoot } from "./analysis.ts";
|
|
2
|
+
import { ConfigError, REPOSITORY_DOCUMENT, type Inputs } from "./config.ts";
|
|
2
3
|
import {
|
|
3
4
|
diagnosticErrors,
|
|
4
5
|
discoverProjects,
|
|
@@ -18,11 +19,9 @@ export interface Writer {
|
|
|
18
19
|
write(text: string): void;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
/** The repository configuration's name at the target root. */
|
|
22
|
-
const REPOSITORY_DOCUMENT = "deadset.json";
|
|
23
|
-
|
|
24
22
|
/** The exit codes the Contract's exit-code table names. */
|
|
25
23
|
const EXIT_CLEAN = 0;
|
|
24
|
+
const EXIT_FINDINGS = 1;
|
|
26
25
|
const EXIT_USAGE = 2;
|
|
27
26
|
const EXIT_FAILURE = 3;
|
|
28
27
|
|
|
@@ -219,6 +218,9 @@ export function run(
|
|
|
219
218
|
if (verb === "print-projects") {
|
|
220
219
|
return printProjectsVerb(args.slice(1), out, err, host, openClient);
|
|
221
220
|
}
|
|
221
|
+
if (verb === "print-roots") {
|
|
222
|
+
return printRootsVerb(args.slice(1), out, err, host, openClient);
|
|
223
|
+
}
|
|
222
224
|
if (verb !== undefined && (VERBS as readonly string[]).includes(verb)) {
|
|
223
225
|
err.write(`deadset-ts: ${verb} is not implemented\n`);
|
|
224
226
|
return EXIT_USAGE;
|
|
@@ -282,6 +284,55 @@ function printProjectsVerb(
|
|
|
282
284
|
}
|
|
283
285
|
}
|
|
284
286
|
|
|
287
|
+
/**
|
|
288
|
+
* One root as a line: the declaration's reference, why it is a root, and the string
|
|
289
|
+
* that named it, separated by tabs. A run of several configurations adds the ones
|
|
290
|
+
* whose project holds the root as a fourth field, and writes the third present and
|
|
291
|
+
* empty where no string named the root, so every line of one run holds the same
|
|
292
|
+
* fields.
|
|
293
|
+
*/
|
|
294
|
+
function rootLine(root: RunRoot, several: boolean): string {
|
|
295
|
+
const line = `${root.ref}\t${root.kind}`;
|
|
296
|
+
if (several) {
|
|
297
|
+
return `${line}\t${root.source}\t${root.configurations.join(" ")}`;
|
|
298
|
+
}
|
|
299
|
+
return root.source === "" ? line : `${line}\t${root.source}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Writes the root set of the run, one root per line, and then names on the error
|
|
304
|
+
* stream, by its issue kind, every configured root or pattern that named nothing;
|
|
305
|
+
* one such string fails the run. The line shape is this command's own rather than a
|
|
306
|
+
* Contract format, so that sort and cut read it.
|
|
307
|
+
*/
|
|
308
|
+
function printRootsVerb(
|
|
309
|
+
args: readonly string[],
|
|
310
|
+
out: Writer,
|
|
311
|
+
err: Writer,
|
|
312
|
+
host: Host,
|
|
313
|
+
openClient: (collectTiming: boolean) => Engine,
|
|
314
|
+
): number {
|
|
315
|
+
let engine: Engine | undefined;
|
|
316
|
+
try {
|
|
317
|
+
const options = readOptions(args);
|
|
318
|
+
const { config, provenance } = resolve(inputsOf(host, options));
|
|
319
|
+
const scope = scopeOf(host, options);
|
|
320
|
+
engine = openClient(false);
|
|
321
|
+
const answer = runRoots(engine, host, scope, config, provenance);
|
|
322
|
+
const several = answer.configurations.length > 1;
|
|
323
|
+
for (const root of answer.roots) {
|
|
324
|
+
out.write(`${rootLine(root, several)}\n`);
|
|
325
|
+
}
|
|
326
|
+
for (const finding of answer.findings) {
|
|
327
|
+
err.write(`${finding.code}: roots.patterns names nothing: ${finding.symbol.ref}\n`);
|
|
328
|
+
}
|
|
329
|
+
return answer.findings.length > 0 ? EXIT_FINDINGS : EXIT_CLEAN;
|
|
330
|
+
} catch (error: unknown) {
|
|
331
|
+
engine?.close();
|
|
332
|
+
return refuse(error, err);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
285
336
|
/** Turns one refusal into the exit code the Contract's table gives it. */
|
|
286
337
|
function refuse(error: unknown, err: Writer): number {
|
|
287
338
|
if (error instanceof ConfigError) {
|
package/src/session.ts
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 type {
|
|
3
3
|
Checker,
|
|
4
4
|
Diagnostic,
|
|
@@ -174,6 +174,11 @@ function viewOf<Brand>(project: Project): ProjectView<Brand> {
|
|
|
174
174
|
isIdentifier(node) ? project.checker.getResolvedSymbol(node) : undefined,
|
|
175
175
|
shorthandValueAt: ({ node }) => project.checker.getShorthandAssignmentValueSymbol(node),
|
|
176
176
|
aliasStepOf: (symbol) => {
|
|
177
|
+
// The step is asked of an alias only: asked of any other symbol, the compiler
|
|
178
|
+
// fails an assertion and the session ends.
|
|
179
|
+
if ((symbol.flags & SymbolFlags.Alias) === 0) {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
177
182
|
const held = project.checker.getImmediateAliasedSymbol(symbol);
|
|
178
183
|
// An alias that resolves to nothing answers with the checker's own unknown
|
|
179
184
|
// symbol, which declares nothing; a symbol that is no alias answers with
|