@cplieger/deadset-ts 8.0.0-dev.15 → 8.0.0-dev.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/dist/src/analysis.js +41 -37
  2. package/dist/src/commands/read.js +6 -7
  3. package/dist/src/conformance-record.js +1 -1
  4. package/dist/src/consumers.js +3 -1
  5. package/dist/src/contract/config.generated.js +1 -1
  6. package/dist/src/contract/contract.generated.js +119 -2
  7. package/dist/src/dependencies.js +79 -48
  8. package/dist/src/entries/build-scripts.js +222 -0
  9. package/dist/src/{configuration-files.js → entries/configuration-files.js} +20 -58
  10. package/dist/src/{convention-rows.js → entries/convention-rows.js} +120 -27
  11. package/dist/src/entries/conventions.js +739 -0
  12. package/dist/src/{html-entries.js → entries/html-entries.js} +3 -3
  13. package/dist/src/{mdx-entries.js → entries/mdx-entries.js} +2 -2
  14. package/dist/src/entries/run-directories.js +77 -0
  15. package/dist/src/entries/sources.js +144 -0
  16. package/dist/src/entries/static-module.js +669 -0
  17. package/dist/src/entries/test-runners.js +684 -0
  18. package/dist/src/{workflow-steps.js → entries/workflow-steps.js} +18 -8
  19. package/dist/src/extensions.js +37 -0
  20. package/dist/src/facts.js +38 -0
  21. package/dist/src/findings/emitters.js +5 -8
  22. package/dist/src/findings/self-check.js +36 -20
  23. package/dist/src/findings-pass.js +5 -6
  24. package/dist/src/framework-lifecycle.js +1 -1
  25. package/dist/src/fs/manifest.js +15 -20
  26. package/dist/src/generated-file.js +172 -29
  27. package/dist/src/graph.js +2 -4
  28. package/dist/src/index.js +1 -1
  29. package/dist/src/injection-container.js +1 -1
  30. package/dist/src/inventory/commonjs.js +62 -17
  31. package/dist/src/inventory/enumerate.js +2 -1
  32. package/dist/src/javascript-file.js +26 -11
  33. package/dist/src/load/classify.js +28 -9
  34. package/dist/src/load/diagnostics.js +8 -1
  35. package/dist/src/load/discover.js +15 -7
  36. package/dist/src/load/plan.js +2 -2
  37. package/dist/src/load/synthesize.js +5 -5
  38. package/dist/src/load/workspace-manifests.js +1 -1
  39. package/dist/src/matrix.js +6 -13
  40. package/dist/src/outside-tests.js +1 -1
  41. package/dist/src/paths.js +16 -0
  42. package/dist/src/platform-calls.js +78 -0
  43. package/dist/src/platform-symbols.js +140 -0
  44. package/dist/src/references.js +202 -32
  45. package/dist/src/resolve.js +1 -1
  46. package/dist/src/{configured-declarations.js → roots/configured-declarations.js} +4 -4
  47. package/dist/src/{entry-point-gaps.js → roots/entry-point-gaps.js} +5 -8
  48. package/dist/src/{manifest.js → roots/manifest.js} +4 -4
  49. package/dist/src/roots/published-reach.js +412 -0
  50. package/dist/src/roots/root-set.js +489 -0
  51. package/dist/src/roots/unseen-loads.js +208 -0
  52. package/dist/src/serialization-contract.js +1 -1
  53. package/dist/src/source-files.js +5 -9
  54. package/dist/src/suppress.js +30 -32
  55. package/dist/src/test-support.js +4 -2
  56. package/dist/src/verbs/analyze.js +13 -1
  57. package/dist/src/verbs/describe.js +5 -3
  58. package/jsr.json +1 -1
  59. package/package.json +2 -2
  60. package/src/analysis.ts +75 -52
  61. package/src/automatic-types.ts +1 -1
  62. package/src/commands/read.ts +6 -8
  63. package/src/conformance-record.ts +1 -1
  64. package/src/consumers.ts +4 -0
  65. package/src/contract/config.generated.ts +1 -1
  66. package/src/contract/contract.generated.ts +138 -2
  67. package/src/dependencies.ts +96 -49
  68. package/src/entries/build-scripts.ts +292 -0
  69. package/src/{configuration-files.ts → entries/configuration-files.ts} +23 -75
  70. package/src/{convention-rows.ts → entries/convention-rows.ts} +167 -28
  71. package/src/{conventions.ts → entries/conventions.ts} +347 -522
  72. package/src/{html-entries.ts → entries/html-entries.ts} +5 -5
  73. package/src/{mdx-entries.ts → entries/mdx-entries.ts} +4 -4
  74. package/src/entries/run-directories.ts +108 -0
  75. package/src/entries/sources.ts +266 -0
  76. package/src/entries/static-module.ts +857 -0
  77. package/src/entries/test-runners.ts +868 -0
  78. package/src/{workflow-steps.ts → entries/workflow-steps.ts} +29 -11
  79. package/src/exempt.ts +3 -2
  80. package/src/extensions.ts +45 -0
  81. package/src/facts.ts +44 -0
  82. package/src/file-facts.ts +1 -1
  83. package/src/findings/emitters.ts +14 -10
  84. package/src/findings/self-check.ts +41 -22
  85. package/src/findings/visibility-narrowing.ts +1 -1
  86. package/src/findings-pass.ts +5 -12
  87. package/src/framework-lifecycle.ts +1 -1
  88. package/src/fs/manifest.ts +25 -21
  89. package/src/generated-file.ts +240 -36
  90. package/src/graph.ts +3 -5
  91. package/src/index.ts +1 -1
  92. package/src/injection-container.ts +1 -1
  93. package/src/inventory/commonjs.ts +102 -17
  94. package/src/inventory/enumerate.ts +2 -1
  95. package/src/javascript-file.ts +29 -12
  96. package/src/load/classify.ts +49 -13
  97. package/src/load/diagnostics.ts +11 -1
  98. package/src/load/discover.ts +27 -11
  99. package/src/load/plan.ts +11 -2
  100. package/src/load/synthesize.ts +6 -6
  101. package/src/load/test-globals.ts +1 -1
  102. package/src/load/workspace-manifests.ts +1 -1
  103. package/src/load/workspace-run.ts +1 -1
  104. package/src/matrix.ts +7 -14
  105. package/src/outside-tests.ts +1 -1
  106. package/src/paths.ts +18 -0
  107. package/src/platform-calls.ts +119 -0
  108. package/src/platform-symbols.ts +205 -0
  109. package/src/references.ts +281 -34
  110. package/src/report.ts +1 -1
  111. package/src/resolve.ts +1 -1
  112. package/src/{configured-declarations.ts → roots/configured-declarations.ts} +7 -7
  113. package/src/{entry-point-gaps.ts → roots/entry-point-gaps.ts} +5 -8
  114. package/src/{manifest.ts → roots/manifest.ts} +5 -5
  115. package/src/roots/published-reach.ts +481 -0
  116. package/src/{roots.ts → roots/root-set.ts} +203 -630
  117. package/src/roots/unseen-loads.ts +307 -0
  118. package/src/serialization-contract.ts +1 -1
  119. package/src/source-files.ts +5 -10
  120. package/src/suppress.ts +48 -33
  121. package/src/test-support.ts +8 -3
  122. package/src/verbs/analyze.ts +17 -4
  123. package/src/verbs/describe.ts +5 -2
  124. package/src/verbs/explain.ts +1 -1
  125. package/dist/src/build-scripts.js +0 -418
  126. package/dist/src/conventions.js +0 -907
  127. package/dist/src/entry-points.js +0 -444
  128. package/dist/src/own-body.js +0 -28
  129. package/dist/src/roots.js +0 -865
  130. package/dist/src/unseen-loads.js +0 -189
  131. package/src/build-scripts.ts +0 -536
  132. package/src/entry-points.ts +0 -584
  133. package/src/own-body.ts +0 -42
  134. package/src/unseen-loads.ts +0 -282
package/dist/src/roots.js DELETED
@@ -1,865 +0,0 @@
1
- /**
2
- * The root set of one project: every declaration kept live without a reference, and why.
3
- * Every root seeds reachability and none counts as a reference; a root of a kind that
4
- * names a caller is live under reference counting too, which the sweep decides, so a
5
- * published export, which only supposes a consumer, stays a reference-counting candidate.
6
- */
7
- import { isArrowFunction, isAssertionExpression, isExpressionWithTypeArguments, isFunctionLikeDeclaration, isIdentifier, isMethodDeclaration, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAssignment, isReturnStatement, isSatisfiesExpression, isPropertyAccessExpression, isQualifiedName, isTypeQueryNode, isTypeReferenceNode, isPropertyDeclaration, isVariableDeclaration, isVariableStatement, SyntaxKind, } from "@typescript/native/ast";
8
- import { aliasChains } from "./alias-chain.js";
9
- import { isGlobalBlock } from "./augmentations.js";
10
- import { filesNamedBy, outsideChain, relativeSpecifiers, } from "./configuration-files.js";
11
- import { conventionEntries } from "./conventions.js";
12
- import { entryPoints } from "./entry-points.js";
13
- import { nodeKey } from "./inventory.js";
14
- import { ancestors, dirnamePath, relativePath, resolvePath } from "./paths.js";
15
- import { byPosition } from "./position.js";
16
- import { UNANSWERED } from "./query.js";
17
- import { sourceFilesOf } from "./source-files.js";
18
- import { typeQueryAliases } from "./type-query-alias.js";
19
- import { bytewise } from "./text/compare.js";
20
- import { holdsManifest } from "./fs/manifest.js";
21
- import { globExpression } from "./text/glob.js";
22
- import { spelledFiles } from "./text/named-files.js";
23
- import { commandFileTexts, workflowCommands } from "./workflow-steps.js";
24
- import { readCommands } from "./commands/read.js";
25
- /** Whether one declaration is written below the directory of a published workspace member. */
26
- function inPublishedMember(held, id, root, members) {
27
- const path = held.symbols.find((symbol) => symbol.id === id)?.position.path;
28
- if (path === undefined || members === undefined) {
29
- return false;
30
- }
31
- const file = resolvePath(root, path);
32
- return [...members].some((dir) => file.startsWith(`${dir}/`));
33
- }
34
- /** The kind of root each role of a manifest entry makes. */
35
- const ENTRY_KINDS = {
36
- import: "manifest-entry",
37
- run: "manifest-binary",
38
- script: "script",
39
- };
40
- /** The rules whose rooted files a manifest target is read back to by name. */
41
- const READ_BACK_RULES = new Set([
42
- "configuration-file",
43
- "configuration-string",
44
- "html-entry",
45
- ]);
46
- /** The extensions a manifest target's name drops: the ones a build writes. */
47
- const OUTPUT_EXTENSION = /\.(?:d\.ts|d\.mts|d\.cts|js|mjs|cjs|jsx)$/u;
48
- /** The extension a source file's name drops. */
49
- const SOURCE_EXTENSION = /\.(?:d\.)?[cm]?[jt]sx?$/u;
50
- /** Whether no `package.json` sits between one file and the manifest's directory, which holds it. */
51
- function ofPackage(host, dir, file) {
52
- const between = ancestors(dirnamePath(file), dir);
53
- return between.length > 0 && !between.slice(0, -1).some((at) => holdsManifest(host, at));
54
- }
55
- /**
56
- * The rooted files one manifest target reads back to by name: its path below the
57
- * manifest's directory, past its first directory where it names one, without its output
58
- * extension, is the name, and a rooted file of the manifest's own package is one whose
59
- * path below the same directory, without its source extension, ends with that name at a
60
- * directory boundary.
61
- */
62
- function readBackByName(host, entry, rooted) {
63
- const dir = resolvePath(host.workingDirectory(), entry.dir);
64
- const below = relativePath(dir, resolvePath(host.workingDirectory(), entry.path));
65
- if (below === undefined || below === "") {
66
- return [];
67
- }
68
- const cut = below.indexOf("/");
69
- const name = (cut === -1 ? below : below.slice(cut + 1)).replace(OUTPUT_EXTENSION, "");
70
- if (name === "") {
71
- return [];
72
- }
73
- const found = new Map();
74
- for (const file of rooted) {
75
- const path = relativePath(dir, file.fileName)?.replace(SOURCE_EXTENSION, "");
76
- if (path !== undefined &&
77
- (path === name || path.endsWith(`/${name}`)) &&
78
- ofPackage(host, dir, file.fileName)) {
79
- found.set(file.fileName, file);
80
- }
81
- }
82
- return [...found.values()];
83
- }
84
- /**
85
- * The program files a manifest target no program holds reaches: the target is a file
86
- * read as a configuration file outside the program is, and each file of the chain it
87
- * starts loads the program files its relative specifiers name, read through the emit
88
- * mapping, as a command's shim loads the compiled entry it runs.
89
- */
90
- function throughOutside(options, targetRoot, files, entry) {
91
- const cwd = options.host.workingDirectory();
92
- const path = resolvePath(cwd, entry.path);
93
- if (options.parse === undefined ||
94
- options.runFiles?.has(path) === true ||
95
- options.host.kindOf(path) !== "file") {
96
- return [];
97
- }
98
- const chain = outsideChain(options.host, targetRoot, files, options.parse, [
99
- { path, manifestDir: resolvePath(cwd, entry.dir) },
100
- ]);
101
- const found = new Map();
102
- for (const { file } of chain) {
103
- for (const specifier of relativeSpecifiers(file)) {
104
- for (const named of filesNamedBy(files, dirnamePath(file.fileName), specifier)) {
105
- found.set(named.fileName, named);
106
- }
107
- }
108
- }
109
- return [...found.values()];
110
- }
111
- /** Whether a configured string carries either special character. */
112
- function isPattern(text) {
113
- return text.includes("*") || text.includes("?");
114
- }
115
- /**
116
- * Whether ref matches a pattern whose only special characters are `*`, which stands
117
- * for any run of characters including the solidus, and `?`, which stands for exactly
118
- * one character counted as a code point. A star keeps the position it last stood at,
119
- * so the shortest run it can stand for is tried first.
120
- */
121
- function matchGlob(pattern, ref) {
122
- let next = 0;
123
- let star = -1;
124
- let resume = 0;
125
- let at = 0;
126
- while (at < ref.length) {
127
- if (next < pattern.length && (pattern[next] === "?" || pattern[next] === ref[at])) {
128
- next += 1;
129
- at += 1;
130
- }
131
- else if (next < pattern.length && pattern[next] === "*") {
132
- star = next;
133
- resume = at;
134
- next += 1;
135
- }
136
- else if (star >= 0) {
137
- resume += 1;
138
- next = star + 1;
139
- at = resume;
140
- }
141
- else {
142
- return false;
143
- }
144
- }
145
- while (next < pattern.length && pattern[next] === "*") {
146
- next += 1;
147
- }
148
- return next === pattern.length;
149
- }
150
- /**
151
- * Whether one configured string names the symbol reference ref. A string carrying
152
- * neither special character names only the reference it spells.
153
- *
154
- * Both sides are read as code points: a character outside the basic plane is two
155
- * string units, and `?` stands for one character.
156
- */
157
- export function matchRef(pattern, ref) {
158
- if (!isPattern(pattern)) {
159
- return pattern === ref;
160
- }
161
- return matchGlob(Array.from(pattern), Array.from(ref));
162
- }
163
- /** The members of one declaration, indexed by the container that holds them. */
164
- function childrenOf(held) {
165
- const byParent = new Map();
166
- for (const symbol of held.symbols) {
167
- const siblings = byParent.get(symbol.parent);
168
- if (siblings === undefined) {
169
- byParent.set(symbol.parent, [symbol]);
170
- }
171
- else {
172
- siblings.push(symbol);
173
- }
174
- }
175
- return byParent;
176
- }
177
- /**
178
- * Per package, the own files its held texts name, as `spelledFiles` reads them, but those a
179
- * root enters with all they export: a command can use no more of such a file than the root
180
- * keeps live. A manifest's are its entries' held texts, and the target root's package has the
181
- * workflow steps, git hooks and makefile recipes beside its own, with the whole text of each
182
- * file `commandFileTexts` reads.
183
- */
184
- function scriptHolds(entries, files, entered, host, targetRoot) {
185
- const rooted = new Set(entered.filter((one) => one.exports && one.names === undefined).map((one) => one.file.fileName));
186
- const spelled = [...files.byName.values()]
187
- .filter((file) => !rooted.has(file.fileName))
188
- .map((file) => ({ file, paths: [file.fileName, ...files.writtenAs(file)] }));
189
- const cwd = host.workingDirectory();
190
- const commands = new Map([
191
- [
192
- resolvePath(cwd, targetRoot),
193
- [
194
- ...workflowCommands(host, targetRoot).flatMap(({ text, dir, writtenDirectory }) => [
195
- readCommands(text, dir).held,
196
- ...(writtenDirectory === undefined ? [] : [writtenDirectory]),
197
- ]),
198
- ...commandFileTexts(host, targetRoot),
199
- ],
200
- ],
201
- ]);
202
- for (const entry of entries) {
203
- if (entry.held !== undefined) {
204
- const dir = resolvePath(cwd, entry.dir);
205
- commands.set(dir, [...(commands.get(dir) ?? []), entry.held]);
206
- }
207
- }
208
- const held = [];
209
- for (const [dir, texts] of commands) {
210
- const named = spelledFiles(texts.join("\n"), dir, spelled);
211
- if (named.length > 0) {
212
- const paths = named.map((file) => relativePath(targetRoot, file.fileName) ?? file.fileName);
213
- held.push({ dir, files: paths });
214
- }
215
- }
216
- return held;
217
- }
218
- /**
219
- * The roots one project holds, and every configured string that named nothing in it. A
220
- * file a manifest member, an entry-file pattern or a tool's configuration names is a root
221
- * with each name of its export table and every link of a re-export chain to the
222
- * declaration it carries; in a library, the published API alone roots what a published
223
- * file exports. A file a runtime executes without reading its exports, a test file or a
224
- * worker, is a root by itself.
225
- */
226
- export function roots(project, held, targetRoot, options) {
227
- const files = sourceFilesOf(project, targetRoot);
228
- const declared = held.declarations;
229
- const fileIds = new Map();
230
- for (const file of files.byName.values()) {
231
- const id = declared.get(nodeKey(file, file));
232
- if (id !== undefined) {
233
- fileIds.set(file.fileName, id);
234
- }
235
- }
236
- const found = new Map();
237
- const add = (id, kind, source) => {
238
- found.set([id, kind, source].join("\u0000"), { id, kind, source });
239
- };
240
- const entered = [];
241
- const enter = (file, kind, source, exports, names) => {
242
- entered.push({ file, kind, source, exports, ...(names === undefined ? {} : { names }) });
243
- };
244
- const points = entryPoints(project, files, options.testFiles, options.host, targetRoot, options.parse, options.conventions?.selected);
245
- const published = [];
246
- const entries = [
247
- ...options.manifest.entries.map((entry) => ({
248
- entry,
249
- publishes: options.publishedAPI &&
250
- entry.published &&
251
- (options.manifest.declaresExports ? entry.member.startsWith("exports") : true),
252
- })),
253
- ...(options.otherPackages ?? []).map((entry) => ({
254
- entry,
255
- publishes: entry.published &&
256
- (options.publishedAPI || options.publishedMembers?.has(entry.dir) === true),
257
- })),
258
- ];
259
- const scripted = entries.flatMap(({ entry }) => entry.role === "script" && entry.held === undefined ? files.named(entry.path) : []);
260
- const readBackTo = [
261
- ...scripted,
262
- ...points.filter((point) => READ_BACK_RULES.has(point.rule)).map((point) => point.file),
263
- ];
264
- const entryFilesOf = new Map();
265
- const entryFiles = (entry) => {
266
- const known = entryFilesOf.get(entry);
267
- if (known !== undefined) {
268
- return known;
269
- }
270
- const found = readEntryFiles(entry);
271
- entryFilesOf.set(entry, found);
272
- return found;
273
- };
274
- const readEntryFiles = (entry) => {
275
- const named = files.named(entry.path);
276
- if (named.length > 0 || entry.role === "script") {
277
- return named;
278
- }
279
- const cwd = options.host.workingDirectory();
280
- const shipped = files.namedAsOutput(resolvePath(cwd, entry.dir), resolvePath(cwd, entry.path));
281
- if (shipped.length > 0) {
282
- return shipped;
283
- }
284
- const byName = readBackByName(options.host, entry, readBackTo);
285
- return byName.length > 0 ? byName : throughOutside(options, targetRoot, files, entry);
286
- };
287
- for (const { entry, publishes } of entries) {
288
- if (publishes) {
289
- published.push(...entryFiles(entry));
290
- }
291
- }
292
- if (options.publishedAPI) {
293
- // A library's bundle hands what its entry exports to every program that loads it.
294
- published.push(...points.filter((point) => point.rule === "bundler-entry").map((point) => point.file));
295
- }
296
- const publishedNames = new Set(published.map((file) => file.fileName));
297
- const patterned = [];
298
- for (const pattern of options.entryFiles) {
299
- const expression = globExpression(pattern);
300
- for (const [path, file] of files.byPath) {
301
- if (expression.test(path)) {
302
- patterned.push({ file, pattern });
303
- }
304
- }
305
- }
306
- const unmapped = [];
307
- const mapped = patterned.map(({ file }) => file);
308
- for (const { entry } of entries) {
309
- if (entry.held !== undefined) {
310
- continue;
311
- }
312
- const named = entryFiles(entry);
313
- if (named.length === 0 && entry.role !== "script") {
314
- unmapped.push(entry);
315
- }
316
- mapped.push(...named);
317
- for (const file of named) {
318
- const kind = ENTRY_KINDS[entry.role];
319
- enter(file, kind, entry.member, !publishedNames.has(file.fileName));
320
- }
321
- }
322
- const unmappedEntries = unmapped.filter((entry) => readBackByName(options.host, entry, mapped).length === 0);
323
- for (const { file, pattern } of patterned) {
324
- enter(file, "entry-file", pattern, true);
325
- }
326
- for (const { file, row } of options.conventions === undefined
327
- ? []
328
- : conventionEntries(options.conventions, files)) {
329
- enter(file, "convention", row, true);
330
- }
331
- for (const point of points) {
332
- enter(point.file, point.rule, point.source, point.exports, point.names);
333
- }
334
- const heldScripts = scriptHolds(entries.map(({ entry }) => entry), files, entered, options.host, targetRoot);
335
- const heldFiles = [...new Set(heldScripts.flatMap((one) => one.files))].flatMap((path) => files.byPath.get(path) ?? []);
336
- for (const file of files.byPath.values()) {
337
- for (const alias of typeQueryAliases(file)) {
338
- const source = alias.type.getText().replace(/\s+/gu, " ");
339
- for (const node of [alias.declaration, ...alias.containers]) {
340
- const id = declared.get(nodeKey(file, node));
341
- if (id !== undefined) {
342
- add(id, "type-query-alias", source);
343
- }
344
- }
345
- }
346
- }
347
- const { tables: exportsOf, named, answered, } = exportTables(project, held, [
348
- ...entered.filter((entry) => entry.exports).map((entry) => entry.file),
349
- ...published,
350
- ...heldFiles,
351
- ]);
352
- for (const entry of entered) {
353
- const id = fileIds.get(entry.file.fileName);
354
- if (id === undefined) {
355
- continue;
356
- }
357
- add(id, entry.kind, entry.source);
358
- if (entry.exports) {
359
- const byName = named.get(entry.file.fileName);
360
- const exported = entry.names === undefined || byName === undefined
361
- ? (exportsOf.get(entry.file.fileName) ?? [])
362
- : entry.names.flatMap((name) => byName.get(name) ?? []);
363
- for (const one of exported) {
364
- add(one, entry.kind, entry.source);
365
- }
366
- }
367
- }
368
- const tagged = releaseTagged(held, files).filter((id) => options.publishedAPI || inPublishedMember(held, id, targetRoot, options.publishedMembers));
369
- const api = published.length > 0 || tagged.length > 0
370
- ? publishedAPI(project, published, exportsOf, held, files.byPath, tagged)
371
- : { ids: [], guessed: [] };
372
- for (const id of api.ids) {
373
- add(id, "published-api", "");
374
- }
375
- const unmatched = configured(held, options.patterns, add);
376
- const sites = new Map(held.symbols.map((symbol) => [symbol.id, symbol.position]));
377
- const ordered = [...found.values()].sort((a, b) => {
378
- const left = sites.get(a.id);
379
- const right = sites.get(b.id);
380
- const site = left === undefined || right === undefined ? 0 : byPosition(left, right);
381
- return site || bytewise(a.kind, b.kind) || bytewise(a.source, b.source);
382
- });
383
- return {
384
- liveUnderReachability: ordered,
385
- unmatched,
386
- unanswered: [
387
- ...(answered ? api.guessed : held.symbols.map((symbol) => symbol.id)),
388
- // A command runs a held file and may read any export of it, so the file, which reaches its
389
- // top level, and its exports are held; a declaration neither reaches is still dead.
390
- ...heldFiles.flatMap((file) => {
391
- const id = fileIds.get(file.fileName);
392
- return [...(id === undefined ? [] : [id]), ...(exportsOf.get(file.fileName) ?? [])];
393
- }),
394
- ],
395
- unmappedEntries,
396
- heldScripts,
397
- };
398
- }
399
- /**
400
- * Every declaration each file's module exports, by the file's name: the declaration
401
- * each name of the export table is, and every link of a re-export's chain. A name
402
- * whose declaration is outside the project's own files names nothing here. One batch
403
- * resolves every file to its module and one request per module reads its export
404
- * table, star re-exports resolved into it. A file that is no module exports nothing.
405
- * `answered` is false where the checker left a table or a link of a chain unanswered.
406
- */
407
- function exportTables(project, held, wanted) {
408
- const unique = [...new Map(wanted.map((file) => [file.fileName, file])).values()];
409
- const tables = new Map();
410
- const named = new Map();
411
- let answered = true;
412
- if (unique.length === 0) {
413
- return { tables, named, answered };
414
- }
415
- const modules = project.symbolsAt(unique.map((file) => project.handle(file)));
416
- const chains = aliasChains(project, held);
417
- const topLevel = (file) => {
418
- const fileId = held.declarations.get(nodeKey(file, file));
419
- return held.symbols.filter((symbol) => symbol.parent === fileId).map((symbol) => symbol.id);
420
- };
421
- unique.forEach((file, index) => {
422
- const module = modules[index];
423
- if (module === undefined) {
424
- tables.set(file.fileName, []);
425
- return;
426
- }
427
- const table = module === UNANSWERED ? UNANSWERED : project.queries.exportsOfModule(module);
428
- if (table === UNANSWERED) {
429
- answered = false;
430
- tables.set(file.fileName, topLevel(file));
431
- return;
432
- }
433
- const ids = new Set();
434
- const byName = new Map();
435
- for (const exported of table) {
436
- for (const target of chains.chainOf(exported)) {
437
- answered &&= !target.guessed;
438
- ids.add(target.id);
439
- byName.set(exported.name, [...(byName.get(exported.name) ?? []), target.id]);
440
- }
441
- }
442
- tables.set(file.fileName, [...ids]);
443
- named.set(file.fileName, byName);
444
- });
445
- return { tables, named, answered };
446
- }
447
- /** A release tag of a documentation comment: `@` and its name, between white space or the ends. */
448
- const RELEASE_TAG = /(?:^|\s)@(?:public|beta|alpha)(?=\s|$)/u;
449
- /**
450
- * Whether the documentation comment of the statement that starts at `start`, the last
451
- * `/** *\/` comment before it with only white space between the two, holds a release tag.
452
- */
453
- function releaseTagIn(text, from, start) {
454
- const leading = text.slice(from, start);
455
- const open = leading.lastIndexOf("/**");
456
- const close = open < 0 ? -1 : leading.indexOf("*/", open + 3);
457
- if (close < 0 || leading.slice(close + 2).trim() !== "") {
458
- return false;
459
- }
460
- return RELEASE_TAG.test(leading.slice(open + 3, close));
461
- }
462
- /** The exported top-level declarations of the program's files a release tag marks. */
463
- function releaseTagged(held, files) {
464
- const exported = new Set(held.symbols.filter((symbol) => symbol.exported).map((one) => one.id));
465
- const found = [];
466
- for (const file of files.byPath.values()) {
467
- for (const statement of file.statements) {
468
- if (!releaseTagIn(file.text, statement.pos, statement.getStart())) {
469
- continue;
470
- }
471
- const nodes = isVariableStatement(statement)
472
- ? statement.declarationList.declarations
473
- : [statement];
474
- for (const node of nodes) {
475
- const id = held.declarations.get(nodeKey(file, node));
476
- if (id !== undefined && exported.has(id)) {
477
- found.push(id);
478
- }
479
- }
480
- }
481
- }
482
- return found;
483
- }
484
- /** The kinds of container whose export table, not its members' visibility, says what it publishes. */
485
- const EXPORTING_CONTAINERS = new Set(["file", "namespace"]);
486
- /**
487
- * Every declaration a consumer of the library can name, a published file itself excluded:
488
- * what the published files export, with a namespace's exported members and any other
489
- * declaration's public and protected ones; a declaration a published one's type names; an
490
- * exported declaration a release tag marks; each declaration a `declare global` block holds
491
- * in a published file or one holding a published declaration; and, as `guessed`, each
492
- * declaration spelled as a type name the checker leaves unanswered.
493
- */
494
- function publishedAPI(project, published, exportsOf, held, byPath, tagged) {
495
- const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
496
- const children = childrenOf(held);
497
- const nodes = declarationNodes(held, byPath);
498
- const chains = aliasChains(project, held);
499
- const reached = new Set();
500
- const entered = new Set();
501
- const guessed = new Set();
502
- const pending = [];
503
- const reach = (id) => {
504
- const file = byId.get(id)?.kind === "file";
505
- if (!(file ? entered : reached).has(id)) {
506
- (file ? entered : reached).add(id);
507
- pending.push(id);
508
- }
509
- };
510
- for (const id of tagged) {
511
- reach(id);
512
- }
513
- for (const file of published) {
514
- for (const id of exportsOf.get(file.fileName) ?? []) {
515
- reach(id);
516
- }
517
- }
518
- const files = new Set(held.symbols.filter((one) => one.kind === "file").map((one) => one.id));
519
- const blocks = held.symbols.filter((one) => isGlobalBlock(one, held, files));
520
- const fileOf = (id) => {
521
- let at = byId.get(id);
522
- while (at !== undefined && at.kind !== "file") {
523
- at = byId.get(at.parent);
524
- }
525
- return at?.id;
526
- };
527
- const publishedFiles = published.flatMap((file) => held.declarations.get(nodeKey(file, file)) ?? []);
528
- const globalsReached = () => {
529
- const typed = new Set([...publishedFiles, ...[...reached].flatMap((id) => fileOf(id) ?? [])]);
530
- const before = pending.length;
531
- for (const block of blocks.filter((one) => typed.has(one.parent))) {
532
- reach(block.id);
533
- for (const member of children.get(block.id) ?? []) {
534
- if (member.kind !== "type-parameter") {
535
- reach(member.id);
536
- }
537
- }
538
- }
539
- return pending.length > before;
540
- };
541
- do {
542
- while (pending.length > 0) {
543
- const round = [];
544
- for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
545
- round.push(next);
546
- const exporting = EXPORTING_CONTAINERS.has(byId.get(next)?.kind ?? "");
547
- for (const member of children.get(next) ?? []) {
548
- const reachable = exporting
549
- ? member.exported
550
- : member.visibility !== "private" && member.visibility !== "private-name";
551
- if (member.kind !== "type-parameter" && reachable) {
552
- reach(member.id);
553
- }
554
- }
555
- }
556
- const named = round.flatMap((id) => {
557
- const symbol = byId.get(id);
558
- return symbol === undefined || symbol.kind === "file" ? [] : nodes.typeNamesOf(symbol);
559
- });
560
- const inferred = inferredTypeSymbols(project, round.flatMap((id) => {
561
- const symbol = byId.get(id);
562
- return symbol === undefined ? [] : nodes.nodesOf(symbol);
563
- }));
564
- for (const symbol of inferred.types) {
565
- for (const target of chains.chainOf(symbol)) {
566
- reach(target.id);
567
- }
568
- }
569
- for (const symbol of inferred.members) {
570
- for (const target of chains.chainOf(symbol)) {
571
- const member = byId.get(target.id);
572
- if (member?.visibility === "public" || member?.visibility === "protected") {
573
- reach(target.id);
574
- }
575
- }
576
- }
577
- project.symbolsAt(named.map((node) => project.handle(node))).forEach((symbol, index) => {
578
- if (symbol === UNANSWERED) {
579
- for (const id of held.byName.get(named[index]?.getText() ?? "") ?? []) {
580
- guessed.add(id);
581
- reach(id);
582
- }
583
- }
584
- else if (symbol !== undefined) {
585
- for (const target of chains.chainOf(symbol)) {
586
- reach(target.id);
587
- }
588
- }
589
- });
590
- }
591
- } while (globalsReached());
592
- return { ids: [...reached], guessed: [...guessed] };
593
- }
594
- /** The type names declarations of the inventory write, each file walked once, when first asked. */
595
- function declarationNodes(held, byPath) {
596
- const walked = new Map();
597
- const nodesIn = (path) => {
598
- const known = walked.get(path);
599
- if (known !== undefined) {
600
- return known;
601
- }
602
- const found = { byId: new Map(), nodes: new Set() };
603
- const file = byPath.get(path);
604
- if (file !== undefined) {
605
- const visit = (node) => {
606
- const id = held.declarations.get(nodeKey(file, node));
607
- if (id !== undefined) {
608
- found.nodes.add(node);
609
- found.byId.set(id, [...(found.byId.get(id) ?? []), node]);
610
- }
611
- node.forEachChild(visit);
612
- };
613
- visit(file);
614
- }
615
- walked.set(path, found);
616
- return found;
617
- };
618
- return {
619
- typeNamesOf: (symbol) => {
620
- const { byId, nodes } = nodesIn(symbol.position.path);
621
- return (byId.get(symbol.id) ?? []).flatMap((declaration) => typeNamesOf(declaration, nodes));
622
- },
623
- nodesOf: (symbol) => nodesIn(symbol.position.path).byId.get(symbol.id) ?? [],
624
- };
625
- }
626
- /** How deep the type arguments and constituents of an inferred type are followed. */
627
- const INFERRED_DEPTH = 4;
628
- /**
629
- * The values one function body returns, each through the type assertions around it: the
630
- * value a caller receives carries the operand's type whatever type the assertion or the
631
- * signature names. A nested function's returns are its own.
632
- */
633
- function returnedValues(body) {
634
- const found = [];
635
- const add = (value) => {
636
- for (let at = value; at !== undefined;) {
637
- found.push(at);
638
- at =
639
- isAssertionExpression(at) || isSatisfiesExpression(at) || isParenthesizedExpression(at)
640
- ? at.expression
641
- : undefined;
642
- }
643
- };
644
- if (!isFunctionLikeDeclaration(body.parent) || body.parent.body !== body) {
645
- return found;
646
- }
647
- if (isArrowFunction(body.parent) && body.kind !== SyntaxKind.Block) {
648
- add(body);
649
- return found;
650
- }
651
- const visit = (node) => {
652
- if (isFunctionLikeDeclaration(node)) {
653
- return;
654
- }
655
- if (isReturnStatement(node) && node.expression !== undefined) {
656
- add(node.expression);
657
- }
658
- node.forEachChild(visit);
659
- };
660
- body.forEachChild(visit);
661
- return found;
662
- }
663
- /** The object literal one returned value is, written there or as the initializer of a variable `body` declares. */
664
- function returnedLiteral(value, body) {
665
- if (isObjectLiteralExpression(value)) {
666
- return value;
667
- }
668
- if (!isIdentifier(value)) {
669
- return undefined;
670
- }
671
- let found;
672
- const visit = (node) => {
673
- if (isVariableDeclaration(node) &&
674
- isIdentifier(node.name) &&
675
- node.name.text === value.text &&
676
- node.initializer !== undefined) {
677
- let at = node.initializer;
678
- while (isAssertionExpression(at) ||
679
- isSatisfiesExpression(at) ||
680
- isParenthesizedExpression(at)) {
681
- at = at.expression;
682
- }
683
- found = isObjectLiteralExpression(at) ? at : found;
684
- }
685
- if (!isFunctionLikeDeclaration(node)) {
686
- node.forEachChild(visit);
687
- }
688
- };
689
- body.forEachChild(visit);
690
- return found;
691
- }
692
- /**
693
- * The values one function body returns, and those each method of an object literal it
694
- * returns returns, to a fixed depth: a caller holding the literal calls its methods, and
695
- * receives what they return.
696
- */
697
- function returnedThroughLiterals(body, depth = 0) {
698
- const values = returnedValues(body);
699
- if (depth >= INFERRED_DEPTH) {
700
- return values;
701
- }
702
- const found = [...values];
703
- for (const value of values) {
704
- for (const property of returnedLiteral(value, body)?.properties ?? []) {
705
- const fn = isMethodDeclaration(property)
706
- ? property
707
- : isPropertyAssignment(property) && isFunctionLikeDeclaration(property.initializer)
708
- ? property.initializer
709
- : undefined;
710
- if (fn?.body !== undefined) {
711
- found.push(...returnedThroughLiterals(fn.body, depth + 1));
712
- }
713
- }
714
- }
715
- return found;
716
- }
717
- /**
718
- * The declarations of the types the compiler infers where a declaration writes none, and
719
- * of the values a function returns: the type of a variable or a property with no
720
- * annotation, the result of a function or a method with no result annotation, and the type
721
- * of each value its body returns, each with the types its type arguments and its union or
722
- * intersection constituents name, to a fixed depth; and as `members` the properties of each
723
- * returned value's type. A question the checker leaves unanswered names nothing.
724
- */
725
- function inferredTypeSymbols(project, declarations) {
726
- const { queries } = project;
727
- const valued = declarations.flatMap((node) => (isVariableDeclaration(node) || isPropertyDeclaration(node)) &&
728
- node.type === undefined &&
729
- !isFunctionLikeDeclaration(node.initializer ?? node)
730
- ? [node.name]
731
- : []);
732
- const returned = declarations.flatMap((node) => {
733
- const signature = isFunctionLikeDeclaration(node)
734
- ? node
735
- : isVariableDeclaration(node) &&
736
- node.initializer !== undefined &&
737
- isFunctionLikeDeclaration(node.initializer)
738
- ? node.initializer
739
- : undefined;
740
- return signature?.body === undefined ? [] : returnedThroughLiterals(signature.body);
741
- });
742
- const types = [];
743
- const returnedTypes = [];
744
- queries.typesAt([...valued, ...returned]).forEach((type, at) => {
745
- if (type !== UNANSWERED && type !== undefined) {
746
- (at < valued.length ? types : returnedTypes).push(type);
747
- }
748
- });
749
- types.push(...returnedTypes);
750
- for (const node of declarations) {
751
- const signature = isFunctionLikeDeclaration(node)
752
- ? node
753
- : isVariableDeclaration(node) &&
754
- node.type === undefined &&
755
- node.initializer !== undefined &&
756
- isFunctionLikeDeclaration(node.initializer)
757
- ? node.initializer
758
- : undefined;
759
- if (signature === undefined || signature.type !== undefined || signature.body === undefined) {
760
- continue;
761
- }
762
- const typed = queries.signatureOf(signature);
763
- const result = typed === UNANSWERED || typed === undefined ? undefined : queries.returnTypeOf(typed);
764
- if (result !== UNANSWERED && result !== undefined) {
765
- types.push(result);
766
- }
767
- }
768
- const found = new Map();
769
- const seen = new Set();
770
- const visit = (type, depth) => {
771
- if (depth > INFERRED_DEPTH || seen.has(type)) {
772
- return;
773
- }
774
- seen.add(type);
775
- const symbol = queries.symbolOfType(type);
776
- if (symbol !== UNANSWERED && symbol !== undefined) {
777
- found.set(symbol.id, symbol);
778
- }
779
- const parts = queries.constituents(type);
780
- const args = queries.typeArguments(type);
781
- for (const next of [
782
- ...(parts === UNANSWERED || (parts.length === 1 && parts[0] === type) ? [] : parts),
783
- ...(args === UNANSWERED ? [] : args),
784
- ]) {
785
- visit(next, depth + 1);
786
- }
787
- };
788
- types.forEach((type) => {
789
- visit(type, 0);
790
- });
791
- // A returned value carries each property of its type, one an anonymous type declares or
792
- // an intersection merges included, which no type name reaches.
793
- const members = new Map();
794
- for (const type of returnedTypes) {
795
- const parts = queries.constituents(type);
796
- for (const one of [type, ...(parts === UNANSWERED ? [] : parts)]) {
797
- const properties = queries.propertiesOf(one);
798
- for (const property of properties === UNANSWERED ? [] : properties) {
799
- members.set(property.id, property);
800
- }
801
- }
802
- }
803
- return { types: [...found.values()], members: [...members.values()] };
804
- }
805
- /**
806
- * The names of types one declaration's own syntax writes: in a type reference, a
807
- * heritage clause or a type query. A function body is not read, and neither is a
808
- * declaration nested in this one, which `nested` holds and which is read on its own.
809
- */
810
- function typeNamesOf(declaration, nested) {
811
- const found = [];
812
- const visit = (node) => {
813
- if (node !== declaration && nested.has(node)) {
814
- return;
815
- }
816
- if (isTypeReferenceNode(node)) {
817
- found.push(isQualifiedName(node.typeName) ? node.typeName.right : node.typeName);
818
- }
819
- else if (isExpressionWithTypeArguments(node)) {
820
- found.push(isPropertyAccessExpression(node.expression) ? node.expression.name : node.expression);
821
- }
822
- else if (isTypeQueryNode(node)) {
823
- found.push(isQualifiedName(node.exprName) ? node.exprName.right : node.exprName);
824
- }
825
- const body = isFunctionLikeDeclaration(node) ? node.body : undefined;
826
- node.forEachChild((child) => {
827
- if (child !== body) {
828
- visit(child);
829
- }
830
- });
831
- };
832
- visit(declaration);
833
- return found;
834
- }
835
- /**
836
- * Keeps every declaration the configuration names and returns every string that
837
- * named none, in configuration order. A string the configuration lists twice is one
838
- * configured root.
839
- */
840
- function configured(held, patterns, add) {
841
- const unmatched = [];
842
- for (const pattern of new Set(patterns)) {
843
- const kind = isPattern(pattern) ? "pattern" : "configured";
844
- let matched = false;
845
- for (const symbol of held.symbols) {
846
- if (matchRef(pattern, symbol.ref)) {
847
- add(symbol.id, kind, pattern);
848
- matched = true;
849
- }
850
- }
851
- if (!matched) {
852
- unmatched.push(pattern);
853
- }
854
- }
855
- return unmatched;
856
- }
857
- /**
858
- * Every configured string that named no declaration in any project of the run, in
859
- * configuration order and each once. The finding is about the configuration, so a string
860
- * naming a declaration of one project has named something, and a run with no project
861
- * names nothing with any of them.
862
- */
863
- export function unmatchedEverywhere(patterns, perProject) {
864
- return [...new Set(patterns)].filter((pattern) => perProject.every((project) => project.unmatched.includes(pattern)));
865
- }