@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
@@ -2,12 +2,25 @@
2
2
  * The generated-file class on TypeScript: every declaration in a file that marks itself
3
3
  * generated, below a directory an applied convention row names as generated, or minified,
4
4
  * is retained, because a generator owns the file and a deletion there is undone at the next
5
- * generation.
5
+ * generation. The same decision is each `generated` entry of the facts document.
6
6
  */
7
7
 
8
+ import {
9
+ isIdentifier,
10
+ isImportDeclaration,
11
+ isPropertyAssignment,
12
+ isStringLiteralLikeNode,
13
+ isVariableStatement,
14
+ NodeFlags,
15
+ type Node,
16
+ type SourceFile,
17
+ } from "@typescript/native/ast";
18
+ import { GENERATORS } from "./contract/contract.generated.ts";
8
19
  import type { DetectorInput, Evidence } from "./exempt.ts";
9
- import { minified } from "./javascript-file.ts";
20
+ import { minifiedBy } from "./javascript-file.ts";
10
21
  import type { SourceFiles } from "./source-files.ts";
22
+ import { bytewise } from "./text/compare.ts";
23
+ import { satisfies } from "./version-range.ts";
11
24
 
12
25
  /** The generated-code header, matched against each line of a leading comment. */
13
26
  const HEADER_LINE = /^\/\/ Code generated .* DO NOT EDIT\.$/u;
@@ -24,9 +37,44 @@ const GENERATED_WORD = /\bgenerated\b/iu;
24
37
  /** The phrases a generator's notice asks a reader not to edit the file by. */
25
38
  const KEEP_OUT = /\b(?:do not edit|will be overwritten|should not make any changes)\b/iu;
26
39
 
27
- /** The comments before a text's first token, a leading hashbang line skipped. */
28
- function leadingComments(text: string): readonly string[] {
29
- const comments: string[] = [];
40
+ /** What decided a file generated, as the facts document's `evidence` names it. */
41
+ type GeneratedEvidence = "header" | "tag" | "notice" | "convention" | "minified";
42
+
43
+ /** The generator row one generated file matches, with the version and the schema files its output states. */
44
+ interface GeneratorMatch {
45
+ readonly row: string;
46
+ readonly version?: string;
47
+ readonly schemaFiles: readonly string[];
48
+ }
49
+
50
+ /** Why one file is generated, and which generator wrote it. */
51
+ export interface GeneratedFact {
52
+ readonly evidence: GeneratedEvidence;
53
+ /** The 1-based line of the comment line that marks the file; 1 for a row or a minified file. */
54
+ readonly line: number;
55
+ /** The marking line's text, the convention row's name, or what decided the file minified. */
56
+ readonly detail: string;
57
+ readonly generator?: GeneratorMatch;
58
+ }
59
+
60
+ /** One line of a leading comment, with its 1-based line in the file. */
61
+ interface CommentLine {
62
+ readonly text: string;
63
+ readonly line: number;
64
+ }
65
+
66
+ /** The comments before a text's first token, a leading hashbang line skipped, each as its lines. */
67
+ function leadingComments(text: string): readonly (readonly CommentLine[])[] {
68
+ const comments: CommentLine[][] = [];
69
+ const lineAt = (offset: number): number =>
70
+ text.slice(0, offset).split(/\r\n|[\n\r\u2028\u2029]/u).length;
71
+ const lines = (from: number, to: number): CommentLine[] => {
72
+ const first = lineAt(from);
73
+ return text
74
+ .slice(from, to)
75
+ .split(/\r\n|[\n\r\u2028\u2029]/u)
76
+ .map((one, index) => ({ text: one, line: first + index }));
77
+ };
30
78
  let at = text.startsWith("\uFEFF") ? 1 : 0;
31
79
  if (text.startsWith("#!", at)) {
32
80
  const end = text.indexOf("\n", at);
@@ -39,14 +87,14 @@ function leadingComments(text: string): readonly string[] {
39
87
  if (text.startsWith("//", at)) {
40
88
  const end = text.slice(at).search(/[\r\n\u2028\u2029]/u);
41
89
  const stop = end === -1 ? text.length : at + end;
42
- comments.push(text.slice(at, stop));
90
+ comments.push(lines(at, stop));
43
91
  at = stop;
44
92
  } else if (text.startsWith("/*", at)) {
45
93
  const end = text.indexOf("*/", at + 2);
46
94
  if (end === -1) {
47
95
  return comments;
48
96
  }
49
- comments.push(text.slice(at, end + 2));
97
+ comments.push(lines(at, end + 2));
50
98
  at = end + 2;
51
99
  } else {
52
100
  return comments;
@@ -54,51 +102,201 @@ function leadingComments(text: string): readonly string[] {
54
102
  }
55
103
  }
56
104
 
57
- /** The detail a file's leading comments mark it generated by, or undefined where they mark nothing. */
58
- export function generatedByComment(text: string): string | undefined {
59
- const comments = leadingComments(text);
60
- if (comments.some((one) => one.split(/\r?\n/u).some((line) => HEADER_LINE.test(line)))) {
61
- return "generated by the header line its leading comments hold";
105
+ /** The mark one file's leading comments make it generated by, or undefined where they mark nothing. */
106
+ export function generatedMark(text: string): Omit<GeneratedFact, "generator"> | undefined {
107
+ const lines = leadingComments(text).flat();
108
+ const header = lines.find((one) => HEADER_LINE.test(one.text));
109
+ if (header !== undefined) {
110
+ return { evidence: "header", line: header.line, detail: header.text };
111
+ }
112
+ const tag = lines.find((one) => GENERATED_TAG.test(one.text));
113
+ if (tag !== undefined) {
114
+ return { evidence: "tag", line: tag.line, detail: tag.text };
62
115
  }
63
- if (comments.some((one) => GENERATED_TAG.test(one))) {
64
- return "generated by the @generated tag its leading comments hold";
116
+ const [first] = lines;
117
+ if (first !== undefined && GENERATED_BY.test(first.text)) {
118
+ return { evidence: "notice", line: first.line, detail: first.text };
65
119
  }
66
- const notice = comments.join("\n");
67
- if (
68
- GENERATED_BY.test(comments[0] ?? "") ||
69
- (GENERATED_WORD.test(notice) && KEEP_OUT.test(notice))
70
- ) {
71
- return "generated by the notice its leading comments hold";
120
+ const notice = lines.map((one) => one.text).join("\n");
121
+ if (GENERATED_WORD.test(notice) && KEEP_OUT.test(notice)) {
122
+ const marked = lines.find((one) => GENERATED_WORD.test(one.text) || KEEP_OUT.test(one.text));
123
+ return marked === undefined
124
+ ? undefined
125
+ : { evidence: "notice", line: marked.line, detail: marked.text };
126
+ }
127
+ return undefined;
128
+ }
129
+
130
+ /** What one form's first group captures, for each line the form matches. */
131
+ function after(lines: readonly CommentLine[], form: RegExp): readonly string[] {
132
+ return lines.flatMap((one) => form.exec(one.text)?.[1] ?? []);
133
+ }
134
+
135
+ /**
136
+ * Where each plugin's output states its version and its schema files, as `references-ts.36`
137
+ * states: a plugin missing here states no version, and one with no schema form names none.
138
+ */
139
+ const STATEMENTS: Readonly<Record<string, { version?: RegExp; schemaFiles?: RegExp }>> = {
140
+ "protoc-gen-es": {
141
+ version: /^\/\/ @generated by protoc-gen-es (\S+)/u,
142
+ schemaFiles: /^\/\/ @generated from file (\S+)/u,
143
+ },
144
+ "protoc-gen-connect-es": {
145
+ version: /^\/\/ @generated by protoc-gen-connect-es (\S+)/u,
146
+ schemaFiles: /^\/\/ @generated from file (\S+)/u,
147
+ },
148
+ "protobuf-ts": {
149
+ version: /^\/\/ @generated by protobuf-ts (\S+)/u,
150
+ schemaFiles: /^\/\/ @generated from protobuf file "([^"]+)"/u,
151
+ },
152
+ "ts-proto": {
153
+ version: /^\/\/\s+protoc-gen-ts_proto\s+(\S+)$/u,
154
+ schemaFiles: /^\/\/ source: (.+)$/u,
155
+ },
156
+ "protoc-gen-grpc-web": {
157
+ version: /^\/\/\s+protoc-gen-grpc-web\s+(\S+)$/u,
158
+ schemaFiles: /^\/\/ source: (.+)$/u,
159
+ },
160
+ "protoc-gen-js": { schemaFiles: /^\/\/ source: (.+)$/u },
161
+ "ts-protoc-gen": { schemaFiles: /^\/\/ file: (.+)$/u },
162
+ };
163
+
164
+ /** A ts-proto service path literal: `/<package>.<service>/<method>`. */
165
+ const SERVICE_PATH = /^\/(?:\w+\.)+\w+\/\w+$/u;
166
+
167
+ /** Whether one ts-proto file writes a service path, as `references-ts.36` reads one. */
168
+ function writesServicePath(file: SourceFile): boolean {
169
+ const named = file.statements.some(
170
+ (statement) =>
171
+ isVariableStatement(statement) &&
172
+ (statement.declarationList.flags & NodeFlags.Const) !== 0 &&
173
+ statement.declarationList.declarations.some(
174
+ (one) =>
175
+ isIdentifier(one.name) &&
176
+ one.name.text.endsWith("ServiceName") &&
177
+ one.initializer !== undefined &&
178
+ isStringLiteralLikeNode(one.initializer),
179
+ ),
180
+ );
181
+ let found = named;
182
+ const visit = (node: Node): void => {
183
+ if (found) {
184
+ return;
185
+ }
186
+ if (
187
+ isPropertyAssignment(node) &&
188
+ isIdentifier(node.name) &&
189
+ node.name.text === "path" &&
190
+ isStringLiteralLikeNode(node.initializer) &&
191
+ SERVICE_PATH.test(node.initializer.text)
192
+ ) {
193
+ found = true;
194
+ return;
195
+ }
196
+ node.forEachChild(visit);
197
+ };
198
+ file.forEachChild(visit);
199
+ return found;
200
+ }
201
+
202
+ /** Whether one file imports a module by its specifier. */
203
+ function imports(file: SourceFile, module: string): boolean {
204
+ return file.statements.some(
205
+ (statement) =>
206
+ isImportDeclaration(statement) &&
207
+ isStringLiteralLikeNode(statement.moduleSpecifier) &&
208
+ statement.moduleSpecifier.text === module,
209
+ );
210
+ }
211
+
212
+ /** The option sets the rows of `references-ts.36` name, by row; every other row's is met by any output. */
213
+ const OPTION_SETS: Readonly<Record<string, (file: SourceFile) => boolean>> = {
214
+ "ts-proto-2-output-schema": (file) => imports(file, "ts-proto-descriptors"),
215
+ "ts-proto-2": writesServicePath,
216
+ "ts-proto-2-types": (file) => !writesServicePath(file),
217
+ };
218
+
219
+ /** The first generator row one file matches, in `generators.json`'s order, as `bindings.3` reads it. */
220
+ export function generatorOf(file: SourceFile): GeneratorMatch | undefined {
221
+ const lines = leadingComments(file.text).flat();
222
+ for (const row of GENERATORS) {
223
+ const header = new RegExp(row.header, "u");
224
+ if (!lines.some((one) => header.test(one.text))) {
225
+ continue;
226
+ }
227
+ const statements = STATEMENTS[row.plugin] ?? {};
228
+ const [version] = statements.version === undefined ? [] : after(lines, statements.version);
229
+ if (
230
+ "versions" in row &&
231
+ (version === undefined ||
232
+ !satisfies(
233
+ version.replace(/^v/u, ""),
234
+ `>=${row.versions.min.replace(/^v/u, "")} <=${row.versions.max.replace(/^v/u, "")}`,
235
+ ))
236
+ ) {
237
+ continue;
238
+ }
239
+ if (OPTION_SETS[row.id]?.(file) === false) {
240
+ continue;
241
+ }
242
+ const schemaFiles =
243
+ statements.schemaFiles === undefined
244
+ ? []
245
+ : [
246
+ ...new Set(
247
+ after(lines, statements.schemaFiles).filter((one) => one.endsWith(".proto")),
248
+ ),
249
+ ].sort(bytewise);
250
+ return { row: row.id, ...(version === undefined ? {} : { version }), schemaFiles };
72
251
  }
73
252
  return undefined;
74
253
  }
75
254
 
76
255
  /**
77
- * Every own file of one project that is generated, by its path below the target root,
78
- * with the detail a retention in it names: its leading comments' mark, else the row
79
- * naming its directory as generated, else its minified text.
256
+ * Every own file of one project that is generated, by its path below the target root, with
257
+ * what decided it: its leading comments' mark, else the row naming its directory as
258
+ * generated, else its minified name or text; and the generator row it matches.
80
259
  */
81
260
  export function generatedFilesOf(
82
261
  files: SourceFiles,
83
262
  byRow: ReadonlyMap<string, string>,
84
- ): ReadonlyMap<string, string> {
85
- const found = new Map<string, string>();
263
+ ): ReadonlyMap<string, GeneratedFact> {
264
+ const found = new Map<string, GeneratedFact>();
86
265
  for (const [path, file] of files.byPath) {
87
266
  const row = byRow.get(path);
88
- const detail =
89
- generatedByComment(file.text) ??
267
+ const minified = row === undefined ? minifiedBy(file.fileName, file.text) : undefined;
268
+ const fact =
269
+ generatedMark(file.text) ??
90
270
  (row === undefined
91
- ? minified(file.fileName, file.text)
92
- ? "generated as a minified JavaScript file"
93
- : undefined
94
- : `generated below a directory the convention row ${row} names`);
95
- if (detail !== undefined) {
96
- found.set(path, detail);
271
+ ? minified === undefined
272
+ ? undefined
273
+ : { evidence: "minified" as const, line: 1, detail: minified }
274
+ : { evidence: "convention" as const, line: 1, detail: row });
275
+ if (fact === undefined) {
276
+ continue;
97
277
  }
278
+ const generator = generatorOf(file);
279
+ found.set(path, generator === undefined ? fact : { ...fact, generator });
98
280
  }
99
281
  return found;
100
282
  }
101
283
 
284
+ /** The detail a retention in a generated file names, from what decided the file. */
285
+ function retentionDetail(fact: GeneratedFact): string {
286
+ switch (fact.evidence) {
287
+ case "header":
288
+ return "generated by the header line its leading comments hold";
289
+ case "tag":
290
+ return "generated by the @generated tag its leading comments hold";
291
+ case "notice":
292
+ return "generated by the notice its leading comments hold";
293
+ case "convention":
294
+ return `generated below a directory the convention row ${fact.detail} names`;
295
+ case "minified":
296
+ return "generated as a minified JavaScript file";
297
+ }
298
+ }
299
+
102
300
  /** The generated-file detector, recording each file's first line. */
103
301
  export function generatedFile<Brand>(input: DetectorInput<Brand>): readonly Evidence[] {
104
302
  const generated = input.generated;
@@ -106,9 +304,15 @@ export function generatedFile<Brand>(input: DetectorInput<Brand>): readonly Evid
106
304
  return [];
107
305
  }
108
306
  return input.held.symbols.flatMap((symbol): Evidence[] => {
109
- const detail = generated.get(symbol.position.path);
110
- return symbol.kind === "file" || detail === undefined
307
+ const fact = generated.get(symbol.position.path);
308
+ return symbol.kind === "file" || fact === undefined
111
309
  ? []
112
- : [{ id: symbol.id, detail, site: { path: symbol.position.path, line: 1, column: 1 } }];
310
+ : [
311
+ {
312
+ id: symbol.id,
313
+ detail: retentionDetail(fact),
314
+ site: { path: symbol.position.path, line: 1, column: 1 },
315
+ },
316
+ ];
113
317
  });
114
318
  }
package/src/graph.ts CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  import type { InventorySymbol, SymbolKind } from "./inventory.ts";
9
9
  import type { Reference } from "./references.ts";
10
- import type { Root, RootKind } from "./roots.ts";
10
+ import type { Root, RootKind } from "./roots/root-set.ts";
11
11
 
12
12
  /** The position of a declaration the inventory does not hold. */
13
13
  export const OUTSIDE = -1;
@@ -39,8 +39,8 @@ const SWEPT: Readonly<Record<SymbolKind, boolean>> = {
39
39
 
40
40
  /**
41
41
  * Whether a root of each kind names a caller, which holds it live under both relations: a
42
- * tool reads what it loads and what its strings name, a runtime runs a test, setup or
43
- * worker file, a page's module script, a command, a script or a workflow step, the manifest
42
+ * tool reads what it loads and what its strings name, a runtime runs a test or setup
43
+ * file, a page's module script, a command, a script or a workflow step, the manifest
44
44
  * names what enters the package, an alias global stands for its export, an outside
45
45
  * declaration names a global interface merging into it, and an entry file, a configured root
46
46
  * and a pattern assert a caller. A published API supposes a consumer: reachability alone.
@@ -61,8 +61,6 @@ const NAMES_A_CALLER: Readonly<Record<RootKind, boolean>> = {
61
61
  "configuration-string": true,
62
62
  "bundler-entry": true,
63
63
  "test-runner": true,
64
- "browser-tests": true,
65
- worker: true,
66
64
  "html-entry": true,
67
65
  "workflow-step": true,
68
66
  "document-import": true,
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { run, SETTING_OPTIONS, type Writer } from "./run.ts";
2
2
  export type { DirectoryEntry, Host, PathKind, TemporaryDirectory } from "./host.ts";
3
- export { DECLINED_CONVENTIONS, type DeclinedConvention } from "./entry-point-gaps.ts";
3
+ export { DECLINED_CONVENTIONS, type DeclinedConvention } from "./roots/entry-point-gaps.ts";
4
4
  export {
5
5
  byPosition,
6
6
  positionKey,
@@ -27,7 +27,7 @@ import {
27
27
  resolveEntries,
28
28
  spelledEntry,
29
29
  type ResolvedEntry,
30
- } from "./configured-declarations.ts";
30
+ } from "./roots/configured-declarations.ts";
31
31
  import type { DetectorInput, Evidence } from "./exempt.ts";
32
32
  import { isParameterProperty, nodeKey } from "./inventory.ts";
33
33
  import { renderPosition } from "./position.ts";
@@ -59,6 +59,7 @@ import {
59
59
  type DeclarationSource,
60
60
  type SymbolKind,
61
61
  } from "../inventory.ts";
62
+ import { callsPlatformBy, lexicalBinding, writtenName, type BindingOf } from "../platform-calls.ts";
62
63
  import type { Component } from "../ref.ts";
63
64
  import { spellsNoBinding } from "../spelled-names.ts";
64
65
  import { unwrapped } from "../unwrap.ts";
@@ -67,13 +68,14 @@ import { unwrapped } from "../unwrap.ts";
67
68
  * The export table of a CommonJS file: the module value `module.exports` is assigned and
68
69
  * the properties written on it, read from the top-level unconditional statements in order,
69
70
  * as Node runs them (https://nodejs.org/api/modules.html#moduleexports). Each entry is
70
- * keyed by the node that writes it, which is the checker's declaration of the export where
71
- * it binds one, so the two name one subject. Any other write of the exports opens the
72
- * table: its entries are kept and the file is listed open.
71
+ * keyed by the node that writes it, the checker's declaration of the export where it binds
72
+ * one. Any other write of the exports opens the table: its entries are kept and the file is
73
+ * listed open. `declarationOf` reads a `require()` call through the file's own scopes.
73
74
  */
74
75
  export function commonJS(
75
76
  typeIsModule: (path: string) => boolean,
76
77
  resolved: Resolved,
78
+ declarationOf: (occurrence: Identifier) => Node | undefined,
77
79
  ): DeclarationSource {
78
80
  return {
79
81
  declare(file: SourceFile, owner: Building, into: Builder): void {
@@ -82,7 +84,12 @@ export function commonJS(
82
84
  }
83
85
  const bound = boundOccurrences(file, resolved);
84
86
  const free = (node: Node): boolean => !bound.has(node);
85
- const table = readTable(file, free, topLevelDeclarations(file));
87
+ const table = readTable(
88
+ file,
89
+ free,
90
+ topLevelDeclarations(file),
91
+ lexicalBinding(declarationOf),
92
+ );
86
93
  const open = table.open || opens(file, free, table.modelled);
87
94
  for (const entry of table.entries.values()) {
88
95
  const record = into.declare(entry.node, owner, entry.kind, entry.name, {
@@ -267,6 +274,8 @@ interface Entry {
267
274
  readonly component: Component;
268
275
  readonly localName: string | undefined;
269
276
  readonly kind: SymbolKind;
277
+ /** The value written under the key, undefined for an accessor. */
278
+ readonly value: Node | undefined;
270
279
  /** The declaration of the file the module value names, which the export then exports. */
271
280
  readonly target?: Node;
272
281
  /** The class whose members the entry holds. */
@@ -279,6 +288,10 @@ interface Table {
279
288
  readonly open: boolean;
280
289
  /** Every write the scan read, which the opening walk then passes over. */
281
290
  readonly modelled: ReadonlySet<Node>;
291
+ /** The value the last assignment of `module.exports` writes, undefined where none does. */
292
+ readonly whole: Node | undefined;
293
+ /** Whether a member write opens the table, apart from a module value the table cannot read. */
294
+ readonly membersOpen: boolean;
282
295
  }
283
296
 
284
297
  const DEFAULT: Component = { text: "default", computed: false };
@@ -288,23 +301,34 @@ function readTable(
288
301
  file: SourceFile,
289
302
  isFree: (node: Node) => boolean,
290
303
  declarations: ReadonlyMap<string, Node>,
304
+ bindingOf: BindingOf,
291
305
  ): Table {
292
306
  const entries = new Map<string, Entry>();
293
307
  const modelled = new Set<Node>();
294
- let open = false;
308
+ // Written inside the closures below, which the compiler's narrowing does not follow.
309
+ let open = false as boolean;
295
310
  let aliasing = true;
311
+ let whole: Node | undefined;
312
+ let wholeOpen = false;
296
313
  const free = (node: Node, name: string): boolean =>
297
314
  isIdentifier(node) && node.text === name && isFree(node);
298
315
  const isModuleExports = (node: Node): node is PropertyAccessExpression =>
299
316
  isPropertyAccessExpression(node) &&
300
317
  free(node.expression, "module") &&
301
318
  node.name.text === "exports";
302
- const isRequire = (node: Node): boolean =>
303
- isCallExpression(node) &&
304
- free(node.expression, "require") &&
305
- node.arguments.length === 1 &&
306
- node.arguments[0] !== undefined &&
307
- isLiteralText(node.arguments[0]);
319
+ const isRequire = (node: Node): boolean => {
320
+ if (!isCallExpression(node) || node.arguments.length !== 1) {
321
+ return false;
322
+ }
323
+ const [argument] = node.arguments;
324
+ const callee = writtenName(node.expression);
325
+ return (
326
+ argument !== undefined &&
327
+ isLiteralText(argument) &&
328
+ callee !== undefined &&
329
+ callsPlatformBy("require", callee, bindingOf)
330
+ );
331
+ };
308
332
 
309
333
  /** What a value written under one key declares. */
310
334
  const entryOf = (
@@ -314,7 +338,7 @@ function readTable(
314
338
  value: Node | undefined,
315
339
  ): Entry => {
316
340
  const held = value === undefined ? undefined : unwrapped(value);
317
- const base = { node, name, component, localName: undefined };
341
+ const base = { node, name, component, localName: undefined, value };
318
342
  if (held === undefined) {
319
343
  return { ...base, kind: "variable" };
320
344
  }
@@ -365,6 +389,7 @@ function readTable(
365
389
  /** The module value one assignment writes, placed at the `exports` name it assigns. */
366
390
  const moduleValue = (node: BinaryExpression, name: Node): void => {
367
391
  entries.clear();
392
+ whole = node.right;
368
393
  const value = unwrapped(node.right);
369
394
  if (isRequire(value)) {
370
395
  return;
@@ -373,7 +398,7 @@ function readTable(
373
398
  value.properties.forEach(literalProperty);
374
399
  return;
375
400
  }
376
- const base = { node, name, component: DEFAULT };
401
+ const base = { node, name, component: DEFAULT, value: node.right };
377
402
  const target = isIdentifier(value) ? declarations.get(value.text) : undefined;
378
403
  if (target !== undefined) {
379
404
  entries.set("default", { ...base, localName: undefined, kind: "export-alias", target });
@@ -389,7 +414,7 @@ function readTable(
389
414
  members: value,
390
415
  });
391
416
  } else {
392
- open = true;
417
+ wholeOpen = true;
393
418
  }
394
419
  };
395
420
 
@@ -422,8 +447,18 @@ function readTable(
422
447
  }
423
448
  } else if (isModuleExports(left)) {
424
449
  modelled.add(expression);
425
- moduleValue(expression, left.name);
426
- aliasing = false;
450
+ const inner = unwrapped(expression.right);
451
+ const chained =
452
+ isBinaryExpression(inner) &&
453
+ inner.operatorToken.kind === SyntaxKind.EqualsToken &&
454
+ free(inner.left, "exports")
455
+ ? inner
456
+ : undefined;
457
+ if (chained !== undefined) {
458
+ modelled.add(chained);
459
+ }
460
+ moduleValue(chained ?? expression, left.name);
461
+ aliasing = chained !== undefined;
427
462
  } else if (isPropertyAccessExpression(left) || isElementAccessExpression(left)) {
428
463
  const object = exportsObject(left.expression);
429
464
  const key = isPropertyAccessExpression(left) ? left.name : left.argumentExpression;
@@ -454,7 +489,57 @@ function readTable(
454
489
  }
455
490
  }
456
491
  }
457
- return { entries, open, modelled };
492
+ return { entries, open: open || wholeOpen, modelled, whole, membersOpen: open };
493
+ }
494
+
495
+ /** One member a CommonJS file's module value holds, as {@link commonJSModuleValue} reads it. */
496
+ interface WrittenMember {
497
+ readonly key: string;
498
+ /** The value written, undefined for an accessor. */
499
+ readonly value: Node | undefined;
500
+ /** The node that writes the member. */
501
+ readonly node: Node;
502
+ }
503
+
504
+ /** The module value a CommonJS file leaves once Node has run its top level. */
505
+ export interface ModuleValue {
506
+ /** The value the last assignment of `module.exports` writes, where that is no object literal. */
507
+ readonly whole: Node | undefined;
508
+ /** The members written on the module value, an object literal assigned to `module.exports` among them. */
509
+ readonly members: readonly WrittenMember[];
510
+ /** Whether a write the table does not model may set any member. */
511
+ readonly open: boolean;
512
+ }
513
+
514
+ /**
515
+ * The module value one file leaves where Node runs it as CommonJS, read by the one table
516
+ * the inventory's export table reads, or undefined where the file writes module syntax.
517
+ * `declarationOf` names the declaration each name resolves to, undefined for none, as a
518
+ * lexical reading of a file outside every program answers it.
519
+ */
520
+ export function commonJSModuleValue(
521
+ file: SourceFile,
522
+ declarationOf: (occurrence: Identifier) => Node | undefined,
523
+ ): ModuleValue | undefined {
524
+ if (writesModuleSyntax(file)) {
525
+ return undefined;
526
+ }
527
+ const free = (node: Node): boolean => {
528
+ const declaration = isIdentifier(node) ? declarationOf(node) : undefined;
529
+ return declaration === undefined || !bindsOwn(declaration, file);
530
+ };
531
+ const table = readTable(file, free, topLevelDeclarations(file), lexicalBinding(declarationOf));
532
+ const held = table.whole === undefined ? undefined : unwrapped(table.whole);
533
+ const members = [...table.entries.values()].flatMap((entry) =>
534
+ entry.component === DEFAULT && held !== undefined && !isObjectLiteralExpression(held)
535
+ ? []
536
+ : [{ key: entry.component.text, value: entry.value, node: entry.node }],
537
+ );
538
+ return {
539
+ whole: held === undefined || isObjectLiteralExpression(held) ? undefined : table.whole,
540
+ members,
541
+ open: table.membersOpen || opens(file, free, table.modelled),
542
+ };
458
543
  }
459
544
 
460
545
  /** Whether one node is `Object.defineProperty`. */
@@ -15,6 +15,7 @@ import { relativePath } from "../paths.ts";
15
15
  import { renderPosition } from "../position.ts";
16
16
  import { isAnswered, UNANSWERED } from "../query.ts";
17
17
  import type { Handle, ProjectView } from "../session.ts";
18
+ import { declarationOf } from "../entries/static-module.ts";
18
19
  import { commonJS, type Resolved } from "./commonjs.ts";
19
20
  import { SYNTAX } from "./syntax.ts";
20
21
 
@@ -34,7 +35,7 @@ export function inventory<Brand>(
34
35
  const moduleOf = packageScope(host, targetRoot);
35
36
  const sources: readonly DeclarationSource[] = [
36
37
  SYNTAX,
37
- commonJS(packageTypeIsModule(host, targetRoot), resolvedIn(project)),
38
+ commonJS(packageTypeIsModule(host, targetRoot), resolvedIn(project), declarationOf),
38
39
  ];
39
40
  const into = new Builder(targetRoot);
40
41
  for (const file of files) {