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

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 (46) hide show
  1. package/dist/src/alias-chain.js +2 -2
  2. package/dist/src/analysis.js +2 -1
  3. package/dist/src/conformance-record.js +1 -1
  4. package/dist/src/consumers.js +1 -1
  5. package/dist/src/contract/contract.generated.js +1 -0
  6. package/dist/src/dependencies.js +2 -1
  7. package/dist/src/entry-points.js +26 -95
  8. package/dist/src/file-facts.js +1 -1
  9. package/dist/src/graph.js +0 -1
  10. package/dist/src/inventory/commonjs.js +462 -0
  11. package/dist/src/inventory/enumerate.js +166 -0
  12. package/dist/src/inventory/syntax.js +236 -0
  13. package/dist/src/inventory.js +305 -705
  14. package/dist/src/load/discover.js +1 -1
  15. package/dist/src/load/package-scope.js +56 -0
  16. package/dist/src/own-body.js +28 -0
  17. package/dist/src/position.js +41 -0
  18. package/dist/src/ref.js +11 -20
  19. package/dist/src/references.js +91 -30
  20. package/dist/src/session.js +3 -0
  21. package/dist/src/suppress-inline.js +33 -81
  22. package/dist/src/unseen-loads.js +106 -17
  23. package/jsr.json +1 -1
  24. package/package.json +2 -2
  25. package/src/alias-chain.ts +2 -2
  26. package/src/analysis.ts +2 -1
  27. package/src/conformance-record.ts +1 -1
  28. package/src/consumers.ts +1 -1
  29. package/src/contract/contract.generated.ts +1 -0
  30. package/src/dependencies.ts +2 -1
  31. package/src/entry-points.ts +30 -107
  32. package/src/file-facts.ts +1 -1
  33. package/src/graph.ts +0 -1
  34. package/src/inventory/commonjs.ts +611 -0
  35. package/src/inventory/enumerate.ts +202 -0
  36. package/src/inventory/syntax.ts +311 -0
  37. package/src/inventory.ts +424 -878
  38. package/src/load/discover.ts +1 -1
  39. package/src/load/package-scope.ts +74 -0
  40. package/src/own-body.ts +42 -0
  41. package/src/position.ts +42 -0
  42. package/src/ref.ts +11 -20
  43. package/src/references.ts +116 -53
  44. package/src/session.ts +17 -1
  45. package/src/suppress-inline.ts +53 -89
  46. package/src/unseen-loads.ts +166 -18
@@ -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;
@@ -1,26 +1,36 @@
1
1
  /**
2
- * The loads of a project's production files whose target the analysis cannot resolve: an
3
- * `import()` or `require()` call whose path is computed and starts with no relative literal
4
- * naming a directory below the calling file's. The files such a load runs are roots the
5
- * analysis does not see, so the configuration's roots are never complete.
2
+ * The loads by a computed path: those whose files the analysis resolves, and those of a
3
+ * project's production files it cannot resolve, an `import()` or `require()` call whose
4
+ * path starts with no relative literal naming a directory below the calling file's. The
5
+ * files such a load runs are roots the analysis does not see, so the configuration's
6
+ * roots are never complete.
6
7
  */
7
8
 
8
9
  import {
9
10
  isBinaryExpression,
10
11
  isCallExpression,
11
12
  isIdentifier,
13
+ isMetaProperty,
12
14
  isNoSubstitutionTemplateLiteral,
15
+ isObjectLiteralExpression,
16
+ isPropertyAccessExpression,
17
+ isPropertyAssignment,
13
18
  isStringLiteral,
14
19
  isTemplateExpression,
15
20
  SyntaxKind,
21
+ type CallExpression,
22
+ type Identifier,
16
23
  type Node,
24
+ type SourceFile,
17
25
  } from "@typescript/native/ast";
26
+ import { literalsOf } from "./entry-points.ts";
27
+ import { hasOwnBody } from "./own-body.ts";
18
28
  import { dirnamePath, relativePath, resolvePath } from "./paths.ts";
19
29
  import { renderPosition, type Position } from "./position.ts";
20
- import { UNANSWERED } from "./query.ts";
21
30
  import type { ProjectView } from "./session.ts";
22
31
  import { sourceFilesOf } from "./source-files.ts";
23
32
  import { unwrapped } from "./unwrap.ts";
33
+ import { globExpression } from "./text/glob.ts";
24
34
 
25
35
  /** One load the analysis cannot resolve, in the configuration whose production file writes it. */
26
36
  export interface UnseenLoad {
@@ -30,7 +40,7 @@ export interface UnseenLoad {
30
40
  }
31
41
 
32
42
  /** The literal text a computed path starts with: a template's head, or a concatenation's first literal. */
33
- export function leadingLiteral(node: Node): string | undefined {
43
+ function leadingLiteral(node: Node): string | undefined {
34
44
  const at = unwrapped(node);
35
45
  if (isTemplateExpression(at)) {
36
46
  return at.head.text;
@@ -45,11 +55,11 @@ export function leadingLiteral(node: Node): string | undefined {
45
55
  }
46
56
 
47
57
  /**
48
- * The directory below `dir` a computed load's leading literal names, which the entry
49
- * rules root every file of, or none where it names a package, `dir` itself or a
50
- * directory holding it, each of which would name files no rule roots.
58
+ * The directory below `dir` a computed load's leading literal names, every file of which
59
+ * the load reaches, or none where it names a package, `dir` itself or a directory holding
60
+ * it, each of which would name every sibling.
51
61
  */
52
- export function computedLoadDirectory(dir: string, prefix: string): string | undefined {
62
+ function computedLoadDirectory(dir: string, prefix: string): string | undefined {
53
63
  if (!(prefix.startsWith("./") || prefix.startsWith("../")) || !prefix.endsWith("/")) {
54
64
  return undefined;
55
65
  }
@@ -57,6 +67,150 @@ export function computedLoadDirectory(dir: string, prefix: string): string | und
57
67
  return target === dir || relativePath(target, dir) !== undefined ? undefined : target;
58
68
  }
59
69
 
70
+ /** One `import()`, `require()` or `import.meta.glob()` call, and the declaration whose code writes it. */
71
+ export interface LoadCall {
72
+ readonly call: CallExpression;
73
+ readonly from: string;
74
+ }
75
+
76
+ /** One file a computed load may load, by its id. */
77
+ interface Loadable {
78
+ readonly file: SourceFile;
79
+ readonly fileId: string;
80
+ }
81
+
82
+ /** One load by a computed path whose files the analysis resolves. */
83
+ interface ComputedLoad {
84
+ readonly call: CallExpression;
85
+ readonly from: string;
86
+ /** The `require` the call names; {@link hasOwnBody} owns which one loads nothing. */
87
+ readonly require: Identifier | undefined;
88
+ readonly files: readonly Loadable[];
89
+ /** Whether the load reads each file as text or as a URL, which references no declaration of it. */
90
+ readonly asset: boolean;
91
+ /**
92
+ * Whether the load is an eager glob, which Vite turns into static imports at the top of
93
+ * the calling module (https://vite.dev/guide/features.html#glob-import), so the file
94
+ * evaluates what it loads wherever the call is written.
95
+ */
96
+ readonly eager: boolean;
97
+ }
98
+
99
+ /** Whether one node is a call that may load a module by a computed path. */
100
+ export function isLoadCall(node: Node): node is CallExpression {
101
+ if (!isCallExpression(node)) {
102
+ return false;
103
+ }
104
+ const callee = node.expression;
105
+ return (
106
+ callee.kind === SyntaxKind.ImportKeyword ||
107
+ (isIdentifier(callee) && callee.text === "require") ||
108
+ isMetaGlob(node)
109
+ );
110
+ }
111
+
112
+ /**
113
+ * The resolved computed loads among one file's load calls, each loading the `candidates`
114
+ * but the file itself: an `import()` or `require()` whose path starts with a relative
115
+ * literal naming a directory below the file's, every candidate below it, and an
116
+ * `import.meta.glob` of relative literal globs, every candidate one matches, each read
117
+ * against the file's directory, as a bundler's context module reads them.
118
+ */
119
+ export function computedLoadsOf(
120
+ file: SourceFile,
121
+ calls: readonly LoadCall[],
122
+ candidates: readonly Loadable[],
123
+ ): readonly ComputedLoad[] {
124
+ const dir = dirnamePath(file.fileName);
125
+ const others = candidates.filter((candidate) => candidate.file !== file);
126
+ return calls.flatMap(({ call, from }): ComputedLoad[] => {
127
+ const [first, options] = call.arguments;
128
+ if (first === undefined) {
129
+ return [];
130
+ }
131
+ if (isMetaGlob(call)) {
132
+ const globs = (literalsOf(first) ?? [])
133
+ .filter((pattern) => pattern.startsWith("./") || pattern.startsWith("../"))
134
+ .map((pattern) => globExpression(resolvePath(dir, pattern)));
135
+ const files = others.filter((candidate) =>
136
+ globs.some((glob) => glob.test(candidate.file.fileName)),
137
+ );
138
+ const asset = readsAsAsset(options);
139
+ return [{ call, from, require: undefined, files, asset, eager: isEager(options) }];
140
+ }
141
+ const prefix = leadingLiteral(first);
142
+ const target = prefix === undefined ? undefined : computedLoadDirectory(dir, prefix);
143
+ if (target === undefined) {
144
+ return [];
145
+ }
146
+ const files = others.filter(
147
+ (candidate) => relativePath(target, candidate.file.fileName) !== undefined,
148
+ );
149
+ const callee = call.expression;
150
+ const require = isIdentifier(callee) ? callee : undefined;
151
+ return [{ call, from, require, files, asset: false, eager: false }];
152
+ });
153
+ }
154
+
155
+ /** Whether one call is `import.meta.glob(...)`. */
156
+ function isMetaGlob(node: CallExpression): boolean {
157
+ if (!isPropertyAccessExpression(node.expression)) {
158
+ return false;
159
+ }
160
+ const { expression, name } = node.expression;
161
+ return (
162
+ isIdentifier(name) &&
163
+ name.text === "glob" &&
164
+ isMetaProperty(expression) &&
165
+ expression.keywordToken === SyntaxKind.ImportKeyword
166
+ );
167
+ }
168
+
169
+ /** The values an `import.meta.glob` options object assigns, each by its literal key. */
170
+ function globOptions(options: Node | undefined): readonly [string, Node][] {
171
+ const literal = options === undefined ? undefined : unwrapped(options);
172
+ if (literal === undefined || !isObjectLiteralExpression(literal)) {
173
+ return [];
174
+ }
175
+ return literal.properties.flatMap((property): [string, Node][] =>
176
+ isPropertyAssignment(property) &&
177
+ (isIdentifier(property.name) || isStringLiteral(property.name))
178
+ ? [[property.name.text, unwrapped(property.initializer)]]
179
+ : [],
180
+ );
181
+ }
182
+
183
+ /** Whether the options of an `import.meta.glob` set `eager` to `true`. */
184
+ function isEager(options: Node | undefined): boolean {
185
+ return globOptions(options).some(
186
+ ([key, value]) => key === "eager" && value.kind === SyntaxKind.TrueKeyword,
187
+ );
188
+ }
189
+
190
+ /** The query parameters that make Vite load a file as its text or its URL, each written bare. */
191
+ const ASSET_QUERY = /(?:^\?|&)(?:raw|url|inline)(?:&|$)/u;
192
+
193
+ /**
194
+ * Whether the options of an `import.meta.glob` read each file as text or as a URL: a
195
+ * `query` string holding a bare `raw`, `url` or `inline`, or an `as` of `raw` or `url`,
196
+ * which Vite reads as that query (https://vite.dev/guide/features.html#glob-import). Any
197
+ * other query is read as loading the module, which keeps more than it may.
198
+ */
199
+ function readsAsAsset(options: Node | undefined): boolean {
200
+ return globOptions(options).some(([key, value]) => {
201
+ if (!isStringLiteral(value) && !isNoSubstitutionTemplateLiteral(value)) {
202
+ return false;
203
+ }
204
+ if (key === "as") {
205
+ return value.text === "raw" || value.text === "url";
206
+ }
207
+ return (
208
+ key === "query" &&
209
+ ASSET_QUERY.test(value.text.startsWith("?") ? value.text : `?${value.text}`)
210
+ );
211
+ });
212
+ }
213
+
60
214
  /** Whether a computed path's leading literal names a package, whose files are none of the target's. */
61
215
  function namesPackage(prefix: string | undefined): boolean {
62
216
  return (
@@ -71,8 +225,7 @@ function namesPackage(prefix: string | undefined): boolean {
71
225
  /**
72
226
  * Every unresolved load the project's production files write, in file then source order.
73
227
  * `testFiles` holds the test files by their path below the target root. A `require` call
74
- * loads a module only where the name resolves to no declaration of the project's own
75
- * files: one the project declares is its own function.
228
+ * loads a module unless {@link hasOwnBody} reads it as the project's own function.
76
229
  */
77
230
  export function unseenLoadsOf<Brand>(
78
231
  project: ProjectView<Brand>,
@@ -116,15 +269,10 @@ export function unseenLoadsOf<Brand>(
116
269
  file.forEachChild(visit);
117
270
  }
118
271
  const requires = sites.flatMap((site) => (site.require === undefined ? [] : [site.require]));
119
- const own = project.ownPaths();
120
272
  const local = new Set<Node>();
121
273
  project.symbolsAt(requires.map((node) => project.handle(node))).forEach((symbol, at) => {
122
274
  const node = requires[at];
123
- if (
124
- node !== undefined &&
125
- symbol !== UNANSWERED &&
126
- symbol?.declarations.some((handle) => own.has(handle.path)) === true
127
- ) {
275
+ if (node !== undefined && typeof symbol === "object" && hasOwnBody(project, symbol)) {
128
276
  local.add(node);
129
277
  }
130
278
  });