@cplieger/deadset-ts 8.0.0-dev.14 → 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 (149) hide show
  1. package/dist/src/alias-chain.js +2 -2
  2. package/dist/src/analysis.js +43 -38
  3. package/dist/src/commands/read.js +6 -7
  4. package/dist/src/conformance-record.js +1 -1
  5. package/dist/src/consumers.js +4 -2
  6. package/dist/src/contract/config.generated.js +1 -1
  7. package/dist/src/contract/contract.generated.js +120 -2
  8. package/dist/src/dependencies.js +81 -49
  9. package/dist/src/entries/build-scripts.js +222 -0
  10. package/dist/src/{configuration-files.js → entries/configuration-files.js} +20 -58
  11. package/dist/src/{convention-rows.js → entries/convention-rows.js} +120 -27
  12. package/dist/src/entries/conventions.js +739 -0
  13. package/dist/src/{html-entries.js → entries/html-entries.js} +3 -3
  14. package/dist/src/{mdx-entries.js → entries/mdx-entries.js} +2 -2
  15. package/dist/src/entries/run-directories.js +77 -0
  16. package/dist/src/entries/sources.js +144 -0
  17. package/dist/src/entries/static-module.js +669 -0
  18. package/dist/src/entries/test-runners.js +684 -0
  19. package/dist/src/{workflow-steps.js → entries/workflow-steps.js} +18 -8
  20. package/dist/src/extensions.js +37 -0
  21. package/dist/src/facts.js +38 -0
  22. package/dist/src/file-facts.js +1 -1
  23. package/dist/src/findings/emitters.js +5 -8
  24. package/dist/src/findings/self-check.js +36 -20
  25. package/dist/src/findings-pass.js +5 -6
  26. package/dist/src/framework-lifecycle.js +1 -1
  27. package/dist/src/fs/manifest.js +15 -20
  28. package/dist/src/generated-file.js +172 -29
  29. package/dist/src/graph.js +2 -5
  30. package/dist/src/index.js +1 -1
  31. package/dist/src/injection-container.js +1 -1
  32. package/dist/src/inventory/commonjs.js +507 -0
  33. package/dist/src/inventory/enumerate.js +167 -0
  34. package/dist/src/inventory/syntax.js +236 -0
  35. package/dist/src/inventory.js +305 -705
  36. package/dist/src/javascript-file.js +26 -11
  37. package/dist/src/load/classify.js +28 -9
  38. package/dist/src/load/diagnostics.js +8 -1
  39. package/dist/src/load/discover.js +16 -8
  40. package/dist/src/load/package-scope.js +56 -0
  41. package/dist/src/load/plan.js +2 -2
  42. package/dist/src/load/synthesize.js +5 -5
  43. package/dist/src/load/workspace-manifests.js +1 -1
  44. package/dist/src/matrix.js +6 -13
  45. package/dist/src/outside-tests.js +1 -1
  46. package/dist/src/paths.js +16 -0
  47. package/dist/src/platform-calls.js +78 -0
  48. package/dist/src/platform-symbols.js +140 -0
  49. package/dist/src/position.js +41 -0
  50. package/dist/src/ref.js +11 -20
  51. package/dist/src/references.js +281 -50
  52. package/dist/src/resolve.js +1 -1
  53. package/dist/src/{configured-declarations.js → roots/configured-declarations.js} +4 -4
  54. package/dist/src/{entry-point-gaps.js → roots/entry-point-gaps.js} +5 -8
  55. package/dist/src/{manifest.js → roots/manifest.js} +4 -4
  56. package/dist/src/roots/published-reach.js +412 -0
  57. package/dist/src/roots/root-set.js +489 -0
  58. package/dist/src/roots/unseen-loads.js +208 -0
  59. package/dist/src/serialization-contract.js +1 -1
  60. package/dist/src/session.js +3 -0
  61. package/dist/src/source-files.js +5 -9
  62. package/dist/src/suppress-inline.js +33 -81
  63. package/dist/src/suppress.js +30 -32
  64. package/dist/src/test-support.js +4 -2
  65. package/dist/src/verbs/analyze.js +13 -1
  66. package/dist/src/verbs/describe.js +5 -3
  67. package/jsr.json +1 -1
  68. package/package.json +3 -3
  69. package/src/alias-chain.ts +2 -2
  70. package/src/analysis.ts +77 -53
  71. package/src/automatic-types.ts +1 -1
  72. package/src/commands/read.ts +6 -8
  73. package/src/conformance-record.ts +1 -1
  74. package/src/consumers.ts +5 -1
  75. package/src/contract/config.generated.ts +1 -1
  76. package/src/contract/contract.generated.ts +139 -2
  77. package/src/dependencies.ts +98 -50
  78. package/src/entries/build-scripts.ts +292 -0
  79. package/src/{configuration-files.ts → entries/configuration-files.ts} +23 -75
  80. package/src/{convention-rows.ts → entries/convention-rows.ts} +167 -28
  81. package/src/{conventions.ts → entries/conventions.ts} +347 -522
  82. package/src/{html-entries.ts → entries/html-entries.ts} +5 -5
  83. package/src/{mdx-entries.ts → entries/mdx-entries.ts} +4 -4
  84. package/src/entries/run-directories.ts +108 -0
  85. package/src/entries/sources.ts +266 -0
  86. package/src/entries/static-module.ts +857 -0
  87. package/src/entries/test-runners.ts +868 -0
  88. package/src/{workflow-steps.ts → entries/workflow-steps.ts} +29 -11
  89. package/src/exempt.ts +3 -2
  90. package/src/extensions.ts +45 -0
  91. package/src/facts.ts +44 -0
  92. package/src/file-facts.ts +2 -2
  93. package/src/findings/emitters.ts +14 -10
  94. package/src/findings/self-check.ts +41 -22
  95. package/src/findings/visibility-narrowing.ts +1 -1
  96. package/src/findings-pass.ts +5 -12
  97. package/src/framework-lifecycle.ts +1 -1
  98. package/src/fs/manifest.ts +25 -21
  99. package/src/generated-file.ts +240 -36
  100. package/src/graph.ts +3 -6
  101. package/src/index.ts +1 -1
  102. package/src/injection-container.ts +1 -1
  103. package/src/inventory/commonjs.ts +696 -0
  104. package/src/inventory/enumerate.ts +203 -0
  105. package/src/inventory/syntax.ts +311 -0
  106. package/src/inventory.ts +424 -878
  107. package/src/javascript-file.ts +29 -12
  108. package/src/load/classify.ts +49 -13
  109. package/src/load/diagnostics.ts +11 -1
  110. package/src/load/discover.ts +28 -12
  111. package/src/load/package-scope.ts +74 -0
  112. package/src/load/plan.ts +11 -2
  113. package/src/load/synthesize.ts +6 -6
  114. package/src/load/test-globals.ts +1 -1
  115. package/src/load/workspace-manifests.ts +1 -1
  116. package/src/load/workspace-run.ts +1 -1
  117. package/src/matrix.ts +7 -14
  118. package/src/outside-tests.ts +1 -1
  119. package/src/paths.ts +18 -0
  120. package/src/platform-calls.ts +119 -0
  121. package/src/platform-symbols.ts +205 -0
  122. package/src/position.ts +42 -0
  123. package/src/ref.ts +11 -20
  124. package/src/references.ts +383 -73
  125. package/src/report.ts +1 -1
  126. package/src/resolve.ts +1 -1
  127. package/src/{configured-declarations.ts → roots/configured-declarations.ts} +7 -7
  128. package/src/{entry-point-gaps.ts → roots/entry-point-gaps.ts} +5 -8
  129. package/src/{manifest.ts → roots/manifest.ts} +5 -5
  130. package/src/roots/published-reach.ts +481 -0
  131. package/src/{roots.ts → roots/root-set.ts} +203 -630
  132. package/src/roots/unseen-loads.ts +307 -0
  133. package/src/serialization-contract.ts +1 -1
  134. package/src/session.ts +17 -1
  135. package/src/source-files.ts +5 -10
  136. package/src/suppress-inline.ts +53 -89
  137. package/src/suppress.ts +48 -33
  138. package/src/test-support.ts +8 -3
  139. package/src/verbs/analyze.ts +17 -4
  140. package/src/verbs/describe.ts +5 -2
  141. package/src/verbs/explain.ts +1 -1
  142. package/dist/src/build-scripts.js +0 -418
  143. package/dist/src/conventions.js +0 -907
  144. package/dist/src/entry-points.js +0 -513
  145. package/dist/src/roots.js +0 -865
  146. package/dist/src/unseen-loads.js +0 -100
  147. package/src/build-scripts.ts +0 -536
  148. package/src/entry-points.ts +0 -661
  149. package/src/unseen-loads.ts +0 -134
@@ -0,0 +1,307 @@
1
+ /**
2
+ * The loads by a computed path: those whose files the analysis resolves, and those it
3
+ * cannot, whose files are roots the analysis does not see, so the configuration's roots
4
+ * are never complete.
5
+ */
6
+
7
+ import {
8
+ isBinaryExpression,
9
+ isCallExpression,
10
+ isIdentifier,
11
+ isMetaProperty,
12
+ isNoSubstitutionTemplateLiteral,
13
+ isPropertyAccessExpression,
14
+ isStringLiteral,
15
+ isTemplateExpression,
16
+ SyntaxKind,
17
+ type CallExpression,
18
+ type Identifier,
19
+ type Node,
20
+ type SourceFile,
21
+ } from "@typescript/native/ast";
22
+ import { membersNamed, textsOf, writtenValue, type Value } from "../entries/static-module.ts";
23
+ import { writtenName } from "../platform-calls.ts";
24
+ import { dirnamePath, relativePath, resolvePath } from "../paths.ts";
25
+ import type { Position } from "../position.ts";
26
+ import { unwrapped } from "../unwrap.ts";
27
+ import { globExpression } from "../text/glob.ts";
28
+
29
+ /** One load the analysis cannot resolve, in the configuration whose production file writes it. */
30
+ export interface UnseenLoad {
31
+ readonly configuration: string;
32
+ /** The position of the call. */
33
+ readonly position: Position;
34
+ }
35
+
36
+ /** The literal text a computed path starts with: a template's head, or a concatenation's first literal. */
37
+ function leadingLiteral(node: Node): string | undefined {
38
+ const at = unwrapped(node);
39
+ if (isTemplateExpression(at)) {
40
+ return at.head.text;
41
+ }
42
+ if (isBinaryExpression(at) && at.operatorToken.kind === SyntaxKind.PlusToken) {
43
+ const left = unwrapped(at.left);
44
+ return isStringLiteral(left) || isNoSubstitutionTemplateLiteral(left)
45
+ ? left.text
46
+ : leadingLiteral(at.left);
47
+ }
48
+ return undefined;
49
+ }
50
+
51
+ /**
52
+ * The directory below `dir` a computed load's leading literal names, every file of which
53
+ * the load reaches, or none where it names a package, `dir` itself or a directory holding
54
+ * it, each of which would name every sibling.
55
+ */
56
+ function computedLoadDirectory(dir: string, prefix: string): string | undefined {
57
+ if (!(prefix.startsWith("./") || prefix.startsWith("../")) || !prefix.endsWith("/")) {
58
+ return undefined;
59
+ }
60
+ const target = resolvePath(dir, prefix);
61
+ return target === dir || relativePath(target, dir) !== undefined ? undefined : target;
62
+ }
63
+
64
+ /** One `import()`, `require()` or `import.meta.glob()` call, and the declaration whose code writes it. */
65
+ export interface LoadCall {
66
+ readonly call: CallExpression;
67
+ readonly from: string;
68
+ }
69
+
70
+ /** One file a computed load may load, by its id. */
71
+ interface Loadable {
72
+ readonly file: SourceFile;
73
+ readonly fileId: string;
74
+ }
75
+
76
+ /** One load by a computed path whose files the analysis resolves. */
77
+ interface ComputedLoad {
78
+ readonly call: CallExpression;
79
+ readonly from: string;
80
+ /** The name a `require()` call is written with; {@link callsPlatform} owns which one loads. */
81
+ readonly require: Identifier | undefined;
82
+ readonly files: readonly Loadable[];
83
+ /** Whether the load reads each file as text or as a URL, which references no declaration of it. */
84
+ readonly asset: boolean;
85
+ /**
86
+ * Whether the analysis names every file the load may load. An unresolved load names none,
87
+ * and leaves the configuration's roots incomplete (confidence.26).
88
+ */
89
+ readonly resolved: boolean;
90
+ /**
91
+ * Whether the load is an eager glob, which Vite turns into static imports at the top of
92
+ * the calling module (https://vite.dev/guide/features.html#glob-import), so the file
93
+ * evaluates what it loads wherever the call is written.
94
+ */
95
+ readonly eager: boolean;
96
+ }
97
+
98
+ /** Whether one node is a call that may load a module by a computed path. */
99
+ export function isLoadCall(node: Node): node is CallExpression {
100
+ if (!isCallExpression(node)) {
101
+ return false;
102
+ }
103
+ return (
104
+ node.expression.kind === SyntaxKind.ImportKeyword ||
105
+ writtenName(node.expression) !== undefined ||
106
+ isMetaGlob(node)
107
+ );
108
+ }
109
+
110
+ /**
111
+ * The files one `import.meta.glob` call loads, as Vite's `toAbsoluteGlob` reads its globs
112
+ * (https://vite.dev/guide/features.html#glob-import): `/` against the project root, `./`
113
+ * and `../` against a literal `base` (read against the root where it starts with `/`) or the
114
+ * calling file's directory, `**` as written, and `!` removing what it matches. Undefined
115
+ * where Vite reads a glob through an alias, the root is unknown, or a glob or base is computed.
116
+ */
117
+ function globbed(
118
+ file: SourceFile,
119
+ first: Node,
120
+ options: Node | undefined,
121
+ others: readonly Loadable[],
122
+ rootOf: (fileName: string) => string | undefined,
123
+ ): readonly Loadable[] | undefined {
124
+ const patterns = textsOf(writtenValue(first), true);
125
+ const bases = globOption(options, "base");
126
+ const [written, ...more] = bases ?? [];
127
+ const base = written === undefined ? undefined : textsOf(written, false);
128
+ if (
129
+ patterns === undefined ||
130
+ bases === undefined ||
131
+ more.length > 0 ||
132
+ (written !== undefined && base === undefined)
133
+ ) {
134
+ return undefined;
135
+ }
136
+ const dir = dirnamePath(file.fileName);
137
+ const root = (): string | undefined => rootOf(file.fileName);
138
+ const against = (at: string): string | undefined => {
139
+ if (!at.startsWith("/")) {
140
+ return resolvePath(dir, at);
141
+ }
142
+ const top = root();
143
+ return top === undefined ? undefined : resolvePath(top, `.${at}`);
144
+ };
145
+ const from = base?.[0] === undefined ? dir : against(base[0]);
146
+ const keep: RegExp[] = [];
147
+ const drop: RegExp[] = [];
148
+ for (const written of patterns) {
149
+ const negated = written.startsWith("!");
150
+ const pattern = negated ? written.slice(1) : written;
151
+ let path: string | undefined;
152
+ if (pattern.startsWith("/")) {
153
+ path = against(pattern);
154
+ } else if (pattern.startsWith("./") || pattern.startsWith("../")) {
155
+ path = from === undefined ? undefined : resolvePath(from, pattern);
156
+ } else if (pattern.startsWith("**")) {
157
+ path = pattern;
158
+ }
159
+ if (path === undefined) {
160
+ return undefined;
161
+ }
162
+ (negated ? drop : keep).push(globExpression(path));
163
+ }
164
+ return others.filter(
165
+ (candidate) =>
166
+ keep.some((glob) => glob.test(candidate.file.fileName)) &&
167
+ !drop.some((glob) => glob.test(candidate.file.fileName)),
168
+ );
169
+ }
170
+
171
+ /**
172
+ * The computed loads among one file's load calls, each loading the `candidates` but the
173
+ * file itself: an `import()` or `require()` whose path starts with a relative literal naming
174
+ * a directory below the file's loads every candidate below it, as a bundler's context module
175
+ * reads it, and an `import.meta.glob` every candidate its globs match ({@link globbed}). A
176
+ * call of a computed path that names no such directory and no package is an unresolved load.
177
+ */
178
+ export function computedLoadsOf(
179
+ file: SourceFile,
180
+ calls: readonly LoadCall[],
181
+ candidates: readonly Loadable[],
182
+ rootOf: (fileName: string) => string | undefined,
183
+ ): readonly ComputedLoad[] {
184
+ const dir = dirnamePath(file.fileName);
185
+ const others = candidates.filter((candidate) => candidate.file !== file);
186
+ return calls.flatMap(({ call, from }): ComputedLoad[] => {
187
+ const [first, options] = call.arguments;
188
+ if (first === undefined) {
189
+ return [];
190
+ }
191
+ if (isMetaGlob(call)) {
192
+ const files = globbed(file, first, options, others, rootOf);
193
+ const asset = readsAsAsset(options);
194
+ return [
195
+ {
196
+ call,
197
+ from,
198
+ require: undefined,
199
+ files: files ?? [],
200
+ resolved: files !== undefined,
201
+ asset,
202
+ eager: isEager(options),
203
+ },
204
+ ];
205
+ }
206
+ const argument = unwrapped(first);
207
+ const require =
208
+ call.expression.kind === SyntaxKind.ImportKeyword ? undefined : writtenName(call.expression);
209
+ if (!mayNamePath(argument)) {
210
+ return [];
211
+ }
212
+ const prefix = leadingLiteral(argument);
213
+ if (namesPackage(prefix)) {
214
+ return [];
215
+ }
216
+ const target = prefix === undefined ? undefined : computedLoadDirectory(dir, prefix);
217
+ const files =
218
+ target === undefined
219
+ ? []
220
+ : others.filter((candidate) => relativePath(target, candidate.file.fileName) !== undefined);
221
+ return [
222
+ { call, from, require, files, resolved: target !== undefined, asset: false, eager: false },
223
+ ];
224
+ });
225
+ }
226
+
227
+ /**
228
+ * Whether a computed argument may be a module path: anything but a string, which names its
229
+ * module, and a value no path is, a number, a boolean, `null`, an object, an array, a
230
+ * function or a class.
231
+ */
232
+ function mayNamePath(argument: Node): boolean {
233
+ return !NO_PATH.has(argument.kind);
234
+ }
235
+
236
+ const NO_PATH: ReadonlySet<SyntaxKind> = new Set([
237
+ SyntaxKind.StringLiteral,
238
+ SyntaxKind.NoSubstitutionTemplateLiteral,
239
+ SyntaxKind.NumericLiteral,
240
+ SyntaxKind.BigIntLiteral,
241
+ SyntaxKind.TrueKeyword,
242
+ SyntaxKind.FalseKeyword,
243
+ SyntaxKind.NullKeyword,
244
+ SyntaxKind.ObjectLiteralExpression,
245
+ SyntaxKind.ArrayLiteralExpression,
246
+ SyntaxKind.ArrowFunction,
247
+ SyntaxKind.FunctionExpression,
248
+ SyntaxKind.ClassExpression,
249
+ SyntaxKind.RegularExpressionLiteral,
250
+ ]);
251
+
252
+ /** Whether one call is `import.meta.glob(...)`. */
253
+ function isMetaGlob(node: CallExpression): boolean {
254
+ if (!isPropertyAccessExpression(node.expression)) {
255
+ return false;
256
+ }
257
+ const { expression, name } = node.expression;
258
+ return (
259
+ isIdentifier(name) &&
260
+ name.text === "glob" &&
261
+ isMetaProperty(expression) &&
262
+ expression.keywordToken === SyntaxKind.ImportKeyword
263
+ );
264
+ }
265
+
266
+ /** The values the options of an `import.meta.glob` write for one key, undefined where a spread may hide one. */
267
+ function globOption(options: Node | undefined, key: string): readonly Value[] | undefined {
268
+ return options === undefined ? [] : membersNamed(writtenValue(options), key);
269
+ }
270
+
271
+ /** Whether the options of an `import.meta.glob` set `eager` to `true`. */
272
+ function isEager(options: Node | undefined): boolean {
273
+ return (
274
+ globOption(options, "eager")?.some((value) => value.kind === "boolean" && value.value) === true
275
+ );
276
+ }
277
+
278
+ /** The query parameters that make Vite load a file as its text or its URL, each written bare. */
279
+ const ASSET_QUERY = /(?:^\?|&)(?:raw|url|inline)(?:&|$)/u;
280
+
281
+ /**
282
+ * Whether the options of an `import.meta.glob` read each file as text or as a URL: a
283
+ * `query` string holding a bare `raw`, `url` or `inline`, or an `as` of `raw` or `url`,
284
+ * which Vite reads as that query (https://vite.dev/guide/features.html#glob-import). Any
285
+ * other query is read as loading the module, which keeps more than it may.
286
+ */
287
+ function readsAsAsset(options: Node | undefined): boolean {
288
+ const texts = (key: string): readonly string[] =>
289
+ (globOption(options, key) ?? []).flatMap((value) =>
290
+ value.kind === "text" ? [value.text] : [],
291
+ );
292
+ return (
293
+ texts("as").some((text) => text === "raw" || text === "url") ||
294
+ texts("query").some((text) => ASSET_QUERY.test(text.startsWith("?") ? text : `?${text}`))
295
+ );
296
+ }
297
+
298
+ /** Whether a computed path's leading literal names a package, whose files are none of the target's. */
299
+ function namesPackage(prefix: string | undefined): boolean {
300
+ return (
301
+ prefix !== undefined &&
302
+ prefix !== "" &&
303
+ !prefix.startsWith(".") &&
304
+ !prefix.startsWith("/") &&
305
+ !/^[A-Za-z][A-Za-z\d+.-]*:/u.test(prefix)
306
+ );
307
+ }
@@ -72,7 +72,7 @@ import {
72
72
  namesADeclaration,
73
73
  resolveEntries,
74
74
  spelledEntry,
75
- } from "./configured-declarations.ts";
75
+ } from "./roots/configured-declarations.ts";
76
76
  import type { DetectorInput, Evidence } from "./exempt.ts";
77
77
  import { nodeKey, type Inventory } from "./inventory.ts";
78
78
  import { renderPosition, type Position } from "./position.ts";
package/src/session.ts CHANGED
@@ -13,7 +13,7 @@ import type {
13
13
  Symbol as TSSymbol,
14
14
  TimingInfo,
15
15
  } from "@typescript/native/sync";
16
- import type { Node, SourceFile } from "@typescript/native/ast";
16
+ import type { Node, SourceFile, StringLiteralLikeNode } from "@typescript/native/ast";
17
17
  import { componentLayer, mapperManifest } from "./component-layer.ts";
18
18
  import type { Host, TemporaryDirectory } from "./host.ts";
19
19
  import { joinPath } from "./paths.ts";
@@ -242,6 +242,11 @@ export interface ProjectView<Brand> {
242
242
  * file it is in.
243
243
  */
244
244
  ownPaths(): ReadonlySet<string>;
245
+ /**
246
+ * The file the compiler's module resolution names for each module specifier of this
247
+ * project's files, in order, or `undefined` where it names none.
248
+ */
249
+ resolvedFileNames(specifiers: readonly StringLiteralLikeNode[]): (string | undefined)[];
245
250
  /** A handle to one node of this project's program. */
246
251
  handle(node: Node): Handle<Brand>;
247
252
  /**
@@ -347,6 +352,17 @@ function viewOf<Brand>(
347
352
  ownKeys ??= new Set(ownSourceFiles().map((file) => file.path));
348
353
  return ownKeys;
349
354
  },
355
+ resolvedFileNames: (specifiers) =>
356
+ batched(engine, specifiers, (node) =>
357
+ project.program.getResolvedModuleFromModuleSpecifier.gen(
358
+ node,
359
+ node.getSourceFile().fileName,
360
+ ),
361
+ ).map((resolved) =>
362
+ resolved === undefined || resolved.resolvedFileName === ""
363
+ ? undefined
364
+ : resolved.resolvedFileName,
365
+ ),
350
366
  handle: (node) => ({ node }),
351
367
  symbolsAt: (handles, cap) =>
352
368
  queries.symbolsAt(
@@ -8,6 +8,7 @@ import type { SourceFile } from "@typescript/native/ast";
8
8
  import { commonDirectory, emittedFrom } from "./load/emit-map.ts";
9
9
  import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
10
10
  import type { ProjectView } from "./session.ts";
11
+ import { OUTPUT_EXTENSIONS } from "./extensions.ts";
11
12
 
12
13
  /** The one character a subpath pattern of a manifest's `exports` holds. */
13
14
  const SUBPATH_WILDCARD = "*";
@@ -15,9 +16,6 @@ const SUBPATH_WILDCARD = "*";
15
16
  /** The extension a source file's name drops when the project writes it out. */
16
17
  const SOURCE_EXTENSION = /\.[cm]?[jt]sx?$/u;
17
18
 
18
- /** The extensions the project may write a source file's output under. */
19
- const OUTPUT_EXTENSIONS = [".js", ".mjs", ".cjs", ".jsx", ".d.ts", ".d.mts", ".d.cts"] as const;
20
-
21
19
  /**
22
20
  * Whether one path is what a subpath pattern stands for: the text before the
23
21
  * wildcard, any run of characters including the solidus, then the text after it.
@@ -90,13 +88,10 @@ function layoutOf<Brand>(
90
88
  }
91
89
 
92
90
  /**
93
- * The lookups over one project's own files.
94
- *
95
- * A path that names an emitted file is read back to the source the project would
96
- * emit it from through the project's own emit mapping and nothing else: the
97
- * extension is one the compiler emits from a source extension, and a path below the
98
- * output directory is read below the directory the compiler emits from, which the
99
- * configuration declares or which is the directory every input file shares.
91
+ * The lookups over one project's own files. A path naming an emitted file is read back to its
92
+ * source through the project's own emit mapping alone: the extension is one the compiler emits
93
+ * from a source extension, and a path below the output directory is read below the directory
94
+ * the compiler emits from, which the configuration declares or every input file shares.
100
95
  */
101
96
  export function sourceFilesOf<Brand>(project: ProjectView<Brand>, targetRoot: string): SourceFiles {
102
97
  const byName = new Map<string, SourceFile>();
@@ -5,18 +5,21 @@
5
5
  */
6
6
 
7
7
  import { CODE } from "./contract/contract.generated.ts";
8
- import {
9
- isConstructorDeclaration,
10
- SyntaxKind,
11
- type Node,
12
- type SourceFile,
13
- } from "@typescript/native/ast";
8
+ import { SyntaxKind, type Node, type SourceFile } from "@typescript/native/ast";
14
9
  import { importBindings } from "./import-bindings.ts";
15
- import { isParameterProperty, nodeKey, type Inventory } from "./inventory.ts";
16
- import { byPosition, positionKey, renderPosition, type Position } from "./position.ts";
10
+ import { nodeKey, type Inventory } from "./inventory.ts";
11
+ import { relativePath } from "./paths.ts";
12
+ import {
13
+ byPosition,
14
+ lineEnd,
15
+ positionKey,
16
+ renderPosition,
17
+ startLine,
18
+ tokenStart,
19
+ type Position,
20
+ } from "./position.ts";
17
21
  import { importRef } from "./ref.ts";
18
22
  import type { ProjectView } from "./session.ts";
19
- import { signatureOf } from "./signatures.ts";
20
23
  import {
21
24
  isPartKind,
22
25
  SuppressionError,
@@ -88,43 +91,6 @@ const LITERALS: ReadonlySet<SyntaxKind> = new Set([
88
91
  SyntaxKind.JsxText,
89
92
  ]);
90
93
 
91
- /** Whether one character ends a line, under the language's line terminators. */
92
- function endsLine(char: string | undefined): boolean {
93
- return char === "\n" || char === "\r" || char === "\u2028" || char === "\u2029";
94
- }
95
-
96
- /** The offset where the line holding `from` ends. */
97
- function lineEnd(text: string, from: number): number {
98
- let at = from;
99
- while (at < text.length && !endsLine(text[at])) {
100
- at += 1;
101
- }
102
- return at;
103
- }
104
-
105
- /**
106
- * The offset of the first token at or after `from`: whitespace, comments and a leading
107
- * hashbang are trivia.
108
- */
109
- export function tokenStart(text: string, from: number): number {
110
- let at = from;
111
- if (at === 0 && text.startsWith("#!")) {
112
- at = lineEnd(text, 0);
113
- }
114
- for (;;) {
115
- if (/\s/u.test(text[at] ?? "")) {
116
- at += 1;
117
- } else if (text.startsWith("//", at)) {
118
- at = lineEnd(text, at);
119
- } else if (text.startsWith("/*", at)) {
120
- const close = text.indexOf("*/", at + 2);
121
- at = close < 0 ? text.length : close + 2;
122
- } else {
123
- return at;
124
- }
125
- }
126
- }
127
-
128
94
  /**
129
95
  * Every `//` line comment of one file, in file order. The text between tokens is read as
130
96
  * the language reads it: a block comment holds no line comment, and the literal tokens the
@@ -191,63 +157,59 @@ function bindingKey(one: Binding): string {
191
157
  return `${one.declared?.id ?? ""}\u0000${one.part.id}`;
192
158
  }
193
159
 
160
+ /** One declaration the inventory places, the declaration that holds its parts, and its lines. */
161
+ interface Placed {
162
+ readonly one: Declared;
163
+ readonly holder: Declared;
164
+ readonly startLines: readonly number[];
165
+ readonly partLines: readonly number[];
166
+ }
167
+
168
+ /** The placed declarations of a project by the path of the file each is in. */
169
+ function placedByPath(
170
+ held: Inventory,
171
+ symbols: ReadonlyMap<string, Declared>,
172
+ ): ReadonlyMap<string, readonly Placed[]> {
173
+ const byPath = new Map<string, Placed[]>();
174
+ for (const [id, placed] of held.placement) {
175
+ const one = symbols.get(id);
176
+ const holder = symbols.get(placed.holder);
177
+ if (one === undefined || holder === undefined) {
178
+ continue;
179
+ }
180
+ const at = byPath.get(one.position.path) ?? [];
181
+ at.push({ one, holder, startLines: placed.startLines, partLines: placed.partLines });
182
+ byPath.set(one.position.path, at);
183
+ }
184
+ return byPath;
185
+ }
186
+
194
187
  /**
195
- * The declarations of one file by the line each begins on, the line of its first token: a
196
- * decorator, a modifier or the keyword. A variable's first token is its statement's
197
- * keyword, and a variable written on a later line of the statement also begins there. A
198
- * function also begins, for a directive, on the line of each of its parameters, and a
199
- * class, for a part kind, on the lines of its constructor and its constructor's parameters.
188
+ * The declarations of one file by the line a directive binds each on, as the inventory
189
+ * places them, and each import binding of the file on its statement's lines.
200
190
  */
201
191
  function declarationLines(
202
192
  file: SourceFile,
193
+ placed: readonly Placed[],
203
194
  held: Inventory,
204
- symbols: ReadonlyMap<string, Declared>,
205
195
  targetRoot: string,
206
196
  ): ReadonlyMap<number, readonly Binding[]> {
207
197
  const below = new Map<number, Binding[]>();
208
- const add = (offset: number, one: Binding): void => {
209
- const line = file.getLineAndCharacterOfPosition(tokenStart(file.text, offset)).line + 1;
198
+ const add = (line: number, one: Binding): void => {
210
199
  const at = below.get(line) ?? [];
211
200
  if (!at.some((known) => bindingKey(known) === bindingKey(one))) {
212
201
  at.push(one);
213
202
  }
214
203
  below.set(line, at);
215
204
  };
216
- const declaredAt = (node: Node): Declared | undefined => {
217
- const id = held.declarations.get(nodeKey(file, node));
218
- return id === undefined ? undefined : symbols.get(id);
219
- };
220
- const visit = (node: Node): void => {
221
- const one = declaredAt(node);
222
- const owner = isConstructorDeclaration(node) ? declaredAt(node.parent) : undefined;
223
- if (owner !== undefined && isConstructorDeclaration(node)) {
224
- for (const at of [node, ...node.parameters.filter((one) => !isParameterProperty(one))]) {
225
- add(at.pos, { declared: undefined, part: owner });
226
- }
205
+ for (const { one, holder, startLines, partLines } of placed) {
206
+ for (const line of startLines) {
207
+ add(line, { declared: one, part: holder });
227
208
  }
228
- const holder =
229
- one !== undefined && isParameterProperty(node) ? declaredAt(node.parent.parent) : one;
230
- if (one !== undefined && holder !== undefined) {
231
- add(node.pos, { declared: one, part: holder });
232
- }
233
- if (one !== undefined) {
234
- // A parameter on a later line of a wrapped signature takes its directive on the
235
- // line above it, inside the parameter list, and the directive binds the function.
236
- for (const parameter of signatureOf(node)?.parameters ?? []) {
237
- add(parameter.pos, { declared: one, part: one });
238
- }
239
- const list = node.parent;
240
- if (
241
- node.kind === SyntaxKind.VariableDeclaration &&
242
- list.kind === SyntaxKind.VariableDeclarationList &&
243
- list.parent.kind === SyntaxKind.VariableStatement
244
- ) {
245
- add(list.parent.pos, { declared: one, part: one });
246
- }
209
+ for (const line of partLines) {
210
+ add(line, { declared: undefined, part: one });
247
211
  }
248
- node.forEachChild(visit);
249
- };
250
- file.forEachChild(visit);
212
+ }
251
213
  // An import binding is a subject of its own, bound on its line as a declaration is.
252
214
  const fileId = held.declarations.get(nodeKey(file, file));
253
215
  const moduleRef = held.symbols.find((symbol) => symbol.id === fileId)?.ref ?? "";
@@ -259,7 +221,7 @@ function declarationLines(
259
221
  statement = statement.parent;
260
222
  }
261
223
  for (const at of [statement.pos, name.getStart()]) {
262
- add(at, { declared: one, part: one });
224
+ add(startLine(file, at), { declared: one, part: one });
263
225
  }
264
226
  }
265
227
  return below;
@@ -296,13 +258,15 @@ export function inlineDirectives<Brand>(
296
258
  .filter((symbol) => symbol.kind !== "file")
297
259
  .map((symbol) => [symbol.id, { id: symbol.id, ref: symbol.ref, position: symbol.position }]),
298
260
  );
261
+ const placed = placedByPath(held, symbols);
299
262
  const found: InlineDirective[] = [];
300
263
  for (const file of project.ownSourceFiles()) {
301
264
  const comments = lineComments(file).filter((comment) => CANDIDATE.test(comment.text));
302
265
  if (comments.length === 0) {
303
266
  continue;
304
267
  }
305
- const below = declarationLines(file, held, symbols, targetRoot);
268
+ const path = relativePath(targetRoot, file.fileName) ?? "";
269
+ const below = declarationLines(file, placed.get(path) ?? [], held, targetRoot);
306
270
  for (const comment of comments) {
307
271
  const site = renderPosition(file, targetRoot, comment.offset);
308
272
  const target = trails(file, comment.offset) ? site.line : site.line + 1;