@goodbones/typescript 0.1.0-beta.1 → 0.1.0-beta.10

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.
@@ -1,263 +1,396 @@
1
- import ts from "typescript";
2
- // The facts, read out of TypeScript's syntax tree. The plugin reads the same
3
- // facts out of oxlint's; `src/adapters/parity.test.ts` holds the two to one
4
- // answer, so a form added here without the matching visitor there fails a test
5
- // rather than a user.
6
- const scriptKindOf = (file) => file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
1
+ import { parseSync, rawTransferSupported, Visitor, } from "oxc-parser";
2
+ // The whole module, as one binding. `export * from "m"`, `export * as ns from
3
+ // "m"`, `import x = require("m")`, `import("m")` and `require("m")` all carry
4
+ // every export of `m` at once, exactly as `import * as ns` does — and are the
5
+ // same way around a rule about a name. A side-effect import carries nothing.
6
+ const wholeModule = (node, local) => [
7
+ { symbol: "*", kind: "namespace", node, local },
8
+ ];
9
+ // The name written at a key or a module export name: an identifier, or a
10
+ // string literal (`import { "a-b" as ab }`, `"c-d": number`). A numeric key,
11
+ // a private `#name` and a computed expression are not names a rule can speak
12
+ // about.
7
13
  const nameOf = (node) => {
8
- if (node === undefined)
14
+ if (node === null)
9
15
  return null;
10
- if (ts.isIdentifier(node))
11
- return node.text;
12
- if (ts.isStringLiteral(node))
13
- return node.text;
16
+ if (node.type === "Identifier")
17
+ return node.name;
18
+ if (node.type === "Literal" && typeof node.value === "string")
19
+ return node.value;
14
20
  return null;
15
21
  };
16
- const bindingsOfImportClause = (clause) => {
17
- if (clause === undefined)
18
- return [];
19
- const found = [];
20
- if (clause.name !== undefined)
21
- found.push({ symbol: "default", kind: "default" });
22
- const bindings = clause.namedBindings;
23
- if (bindings !== undefined) {
24
- if (ts.isNamespaceImport(bindings))
25
- found.push({ symbol: "*", kind: "namespace" });
26
- else {
27
- for (const element of bindings.elements) {
28
- const symbol = nameOf(element.propertyName ?? element.name);
29
- if (symbol !== null)
30
- found.push({ symbol, kind: "named" });
31
- }
22
+ const bindingOf = (specifier) => {
23
+ const local = specifier.local.name;
24
+ switch (specifier.type) {
25
+ case "ImportSpecifier": {
26
+ const symbol = nameOf(specifier.imported);
27
+ return symbol === null ? null : { symbol, kind: "named", node: specifier, local };
32
28
  }
29
+ case "ImportDefaultSpecifier":
30
+ return { symbol: "default", kind: "default", node: specifier, local };
31
+ case "ImportNamespaceSpecifier":
32
+ return { symbol: "*", kind: "namespace", node: specifier, local };
33
33
  }
34
- return found;
35
34
  };
36
- // The whole module, as one binding. `export * from "m"`, `export * as ns from
37
- // "m"`, `import x = require("m")`, `import("m")` and `require("m")` all carry
38
- // every export of `m` at once, exactly as `import * as ns` does — and are the
39
- // same way around a rule about a name. A side-effect import carries nothing.
40
- const WHOLE_MODULE = [{ symbol: "*", kind: "namespace" }];
41
- // `export { a } from "m"` — `propertyName` is the name in the source module when
42
- // the export is renamed, so it is the one the policy is about.
43
- const bindingsOfExportClause = (clause) => {
44
- if (clause === undefined || ts.isNamespaceExport(clause))
45
- return WHOLE_MODULE;
35
+ // `export { a as b } from "m"` `local` is the name in the source module, so
36
+ // it is the one the policy is about.
37
+ const reexportedBindings = (node) => {
46
38
  const found = [];
47
- for (const element of clause.elements) {
48
- const symbol = nameOf(element.propertyName ?? element.name);
39
+ for (const specifier of node.specifiers) {
40
+ const symbol = nameOf(specifier.local);
49
41
  if (symbol !== null)
50
- found.push({ symbol, kind: "named" });
42
+ found.push({ symbol, kind: "named", node: specifier, local: symbol });
51
43
  }
52
44
  return found;
53
45
  };
54
- // The type literals written in a type, through intersections, unions and
55
- // parentheses: `type Port = Base & ({ a(): void } | { b(): void })` declares `a`
56
- // and `b`. A reference is not followed — `Base`'s members are declared where
57
- // `Base` is, and are reported there under its own name.
46
+ // The type literals written in a type, through intersections and unions:
47
+ // `type Port = Base & ({ a(): void } | { b(): void })` declares `a` and `b`. A
48
+ // reference is not followed — `Base`'s members are declared where `Base` is,
49
+ // and are reported there under its own name. The parenthesised form is read
50
+ // too, for a tree parsed with `preserveParens`; oxlint's and `factsOfText`'s
51
+ // carry none.
58
52
  const literalsOf = (node) => {
59
- if (ts.isTypeLiteralNode(node))
60
- return [node];
61
- if (ts.isIntersectionTypeNode(node) || ts.isUnionTypeNode(node)) {
62
- return node.types.flatMap(literalsOf);
53
+ switch (node.type) {
54
+ case "TSTypeLiteral":
55
+ return [node];
56
+ case "TSIntersectionType":
57
+ case "TSUnionType":
58
+ return node.types.flatMap(literalsOf);
59
+ case "TSParenthesizedType":
60
+ return literalsOf(node.typeAnnotation);
61
+ default:
62
+ return [];
63
63
  }
64
- if (ts.isParenthesizedTypeNode(node))
65
- return literalsOf(node.type);
66
- return [];
67
64
  };
68
- // The member shapes that carry a name a vocabulary rule can speak about: a
69
- // property or method signature in a type, a property, method or accessor in a
70
- // class. Mirrors the plugin's `DECLARED_MEMBER_TYPES`.
71
- const isNamedMember = (member) => ts.isPropertySignature(member) ||
72
- ts.isMethodSignature(member) ||
73
- ts.isPropertyDeclaration(member) ||
74
- ts.isMethodDeclaration(member) ||
75
- ts.isGetAccessorDeclaration(member) ||
76
- ts.isSetAccessorDeclaration(member);
77
- const calleeNameOf = (expression) => {
78
- if (ts.isIdentifier(expression))
79
- return expression.text;
80
- if (ts.isPropertyAccessExpression(expression) && ts.isIdentifier(expression.name)) {
81
- return expression.name.text;
65
+ // The key of a member shape that carries a name a vocabulary rule can speak
66
+ // about: a property or method signature in a type; a property, method,
67
+ // accessor or auto-accessor in a class. A computed key is no name, and neither
68
+ // is an index, call or construct signature, a static block, or a constructor.
69
+ const keyOf = (member) => {
70
+ switch (member.type) {
71
+ case "TSPropertySignature":
72
+ case "TSMethodSignature":
73
+ case "PropertyDefinition":
74
+ case "TSAbstractPropertyDefinition":
75
+ case "AccessorProperty":
76
+ case "TSAbstractAccessorProperty":
77
+ return member.computed ? null : member.key;
78
+ case "MethodDefinition":
79
+ case "TSAbstractMethodDefinition":
80
+ return member.computed || member.kind === "constructor" ? null : member.key;
81
+ default:
82
+ return null;
82
83
  }
84
+ };
85
+ // The name a call is made by: `f()` and `x.f()` are both `f`. `x[f]()` and
86
+ // `x["f"]()` are not — a computed property is not a name a vocabulary rule can
87
+ // speak about — and neither is a private `x.#f()`.
88
+ const calleeNameOf = (callee) => {
89
+ if (callee.type === "Identifier")
90
+ return callee.name;
91
+ if (callee.type === "MemberExpression" && !callee.computed)
92
+ return nameOf(callee.property);
83
93
  return null;
84
94
  };
85
- // The parse alone, for a source that need not be on disk — the CLI reads
86
- // every file through it, a source probe is checked through it at load, and the
87
- // parity suite feeds both adapters the same snippet through it.
88
- export const factsOfText = (file, text) => {
89
- const parsed = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, scriptKindOf(file));
90
- const specifiers = [];
91
- const bindings = new Map();
92
- const memberSites = [];
93
- const record = (specifier, found) => {
94
- if (!bindings.has(specifier)) {
95
- specifiers.push(specifier);
96
- bindings.set(specifier, []);
97
- }
98
- const existing = bindings.get(specifier);
99
- if (existing !== undefined)
100
- for (const binding of found)
101
- existing.push(binding);
102
- };
103
- // The members written in a declaration, under that declaration's name and
104
- // kind. A computed key is not a name a vocabulary rule can speak about;
105
- // neither is a private `#name`, an index, call or construct signature, or a
106
- // constructor.
107
- const declared = (declaration, declares, members) => {
108
- for (const member of members) {
109
- if (!isNamedMember(member))
110
- continue;
111
- if (ts.isComputedPropertyName(member.name) || ts.isPrivateIdentifier(member.name))
112
- continue;
113
- const name = nameOf(member.name);
114
- if (name !== null) {
115
- memberSites.push({ file, subject: "members", name, in: declaration, declares });
116
- }
117
- }
118
- };
119
- const visit = (node) => {
120
- if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
121
- // A side-effect import carries no bindings but is still an edge — the
122
- // `import "server-only"` form a regex cannot see.
123
- record(node.moduleSpecifier.text, bindingsOfImportClause(node.importClause));
124
- }
125
- else if (ts.isExportDeclaration(node) &&
126
- node.moduleSpecifier !== undefined &&
127
- ts.isStringLiteral(node.moduleSpecifier)) {
128
- record(node.moduleSpecifier.text, bindingsOfExportClause(node.exportClause));
129
- }
130
- else if (ts.isImportEqualsDeclaration(node) &&
131
- ts.isExternalModuleReference(node.moduleReference) &&
132
- ts.isStringLiteral(node.moduleReference.expression)) {
133
- record(node.moduleReference.expression.text, WHOLE_MODULE);
134
- }
135
- else if (ts.isCallExpression(node)) {
136
- const [first] = node.arguments;
137
- const isModuleCall = node.expression.kind === ts.SyntaxKind.ImportKeyword ||
138
- (ts.isIdentifier(node.expression) && node.expression.text === "require");
139
- if (isModuleCall && first !== undefined && ts.isStringLiteral(first)) {
140
- record(first.text, WHOLE_MODULE);
141
- }
142
- const callee = calleeNameOf(node.expression);
143
- if (callee !== null)
144
- memberSites.push({ file, subject: "calls", name: callee });
145
- }
146
- else if (ts.isTypeAliasDeclaration(node)) {
147
- declared(node.name.text, "type", literalsOf(node.type).flatMap((literal) => literal.members));
148
- }
149
- else if (ts.isInterfaceDeclaration(node)) {
150
- declared(node.name.text, "interface", node.members);
151
- }
152
- else if (ts.isClassDeclaration(node) && node.name !== undefined) {
153
- // A class body is a declaration with members like any other. An anonymous
154
- // class (`export default class {}`) has no name for `in` and is not read;
155
- // neither is a class expression, which is a value.
156
- declared(node.name.text, "class", node.members);
157
- }
158
- ts.forEachChild(node, visit);
159
- };
160
- visit(parsed);
161
- return { specifiers, bindings, memberSites, exportSites: exportSitesOf(file, parsed) };
95
+ const requireSpecifierOf = (node) => {
96
+ if (node.callee.type !== "Identifier" || node.callee.name !== "require")
97
+ return null;
98
+ const [first] = node.arguments;
99
+ return first?.type === "Literal" && typeof first.value === "string" ? first.value : null;
100
+ };
101
+ // The identifiers a binding pattern introduces.
102
+ const patternNames = (pattern) => {
103
+ switch (pattern.type) {
104
+ case "Identifier":
105
+ return [pattern.name];
106
+ case "ObjectPattern":
107
+ return pattern.properties.flatMap((property) => patternNames(property.type === "Property" ? property.value : property.argument));
108
+ case "ArrayPattern":
109
+ return pattern.elements.flatMap((element) => (element === null ? [] : patternNames(element)));
110
+ case "AssignmentPattern":
111
+ return patternNames(pattern.left);
112
+ case "RestElement":
113
+ return patternNames(pattern.argument);
114
+ }
115
+ };
116
+ // `namespace A.B {}` declares `A`; `declare module "m" {}` declares no name a
117
+ // module's surface can carry.
118
+ const moduleNameOf = (node) => {
119
+ const id = node.id;
120
+ if (id.type !== "TSQualifiedName")
121
+ return nameOf(id);
122
+ let left = id.left;
123
+ while (left.type === "TSQualifiedName")
124
+ left = left.left;
125
+ return left.type === "Identifier" ? left.name : null;
162
126
  };
163
- const hasModifier = (node, kind) => ts.canHaveModifiers(node) && (ts.getModifiers(node) ?? []).some((one) => one.kind === kind);
164
127
  // The names a declaration statement introduces, with what it declares them as.
165
128
  // A destructuring pattern introduces names too, but not ones a surface rule
166
129
  // judges by declaration kind; they read as `variable` like the rest.
167
- const declaredNamesOf = (statement) => {
168
- if (ts.isFunctionDeclaration(statement)) {
169
- return statement.name === undefined ? [] : [[statement.name.text, "function"]];
130
+ const declaredNamesOf = (declaration) => {
131
+ const named = (name, declares) => (name === null ? [] : [[name, declares]]);
132
+ switch (declaration.type) {
133
+ case "VariableDeclaration":
134
+ return declaration.declarations.flatMap((one) => patternNames(one.id).map((name) => [name, "variable"]));
135
+ case "FunctionDeclaration":
136
+ case "TSDeclareFunction":
137
+ return named(declaration.id?.name ?? null, "function");
138
+ case "ClassDeclaration":
139
+ return named(declaration.id?.name ?? null, "class");
140
+ case "TSTypeAliasDeclaration":
141
+ return named(declaration.id.name, "type");
142
+ case "TSInterfaceDeclaration":
143
+ return named(declaration.id.name, "interface");
144
+ case "TSEnumDeclaration":
145
+ return named(declaration.id.name, "enum");
146
+ case "TSModuleDeclaration":
147
+ return named(declaration.global ? declaration.id.name : moduleNameOf(declaration), "other");
148
+ default:
149
+ return [];
170
150
  }
171
- if (ts.isClassDeclaration(statement)) {
172
- return statement.name === undefined ? [] : [[statement.name.text, "class"]];
173
- }
174
- if (ts.isVariableStatement(statement)) {
175
- const names = [];
176
- const collect = (binding) => {
177
- if (ts.isIdentifier(binding))
178
- names.push([binding.text, "variable"]);
179
- else {
180
- for (const element of binding.elements) {
181
- if (ts.isBindingElement(element))
182
- collect(element.name);
183
- }
184
- }
185
- };
186
- for (const declaration of statement.declarationList.declarations)
187
- collect(declaration.name);
188
- return names;
151
+ };
152
+ // The declaration an `export default …` carries, when it is one — a function,
153
+ // class or interface, named or not — rather than an expression.
154
+ const defaultDeclaration = (node) => {
155
+ const declaration = node.declaration;
156
+ switch (declaration.type) {
157
+ case "FunctionDeclaration":
158
+ case "TSDeclareFunction":
159
+ case "ClassDeclaration":
160
+ case "TSInterfaceDeclaration":
161
+ return declaration;
162
+ default:
163
+ return null;
189
164
  }
190
- if (ts.isTypeAliasDeclaration(statement))
191
- return [[statement.name.text, "type"]];
192
- if (ts.isInterfaceDeclaration(statement))
193
- return [[statement.name.text, "interface"]];
194
- if (ts.isEnumDeclaration(statement))
195
- return [[statement.name.text, "enum"]];
196
- if (ts.isModuleDeclaration(statement) && ts.isIdentifier(statement.name)) {
197
- return [[statement.name.text, "other"]];
165
+ };
166
+ // What `export default …` declares: a function, class or interface by its
167
+ // shape, named or not; an identifier by the declaration it names in this
168
+ // file; any other expression as one.
169
+ const defaultDeclares = (node, locals) => {
170
+ const declaration = node.declaration;
171
+ switch (declaration.type) {
172
+ case "FunctionDeclaration":
173
+ case "TSDeclareFunction":
174
+ return "function";
175
+ case "ClassDeclaration":
176
+ return "class";
177
+ case "TSInterfaceDeclaration":
178
+ return "interface";
179
+ case "Identifier":
180
+ return locals.get(declaration.name) ?? "expression";
181
+ default:
182
+ return "expression";
198
183
  }
199
- return [];
200
184
  };
201
- // A file's surface: what its top-level statements export, in source order. An
202
- // `export` inside a namespace body is that namespace's, not the module's.
203
- const exportSitesOf = (file, parsed) => {
185
+ // A file's surface: what its top-level statements export, in source order,
186
+ // read off the program body rather than by visiting export nodes — an `export`
187
+ // inside a namespace body is that namespace's, not the module's, and reading
188
+ // the top level directly is what says so without a parent pointer. `export =`
189
+ // is a CommonJS surface, not a module's, and is stepped over.
190
+ const exportSitesOf = (file, body) => {
204
191
  const locals = new Map();
205
- for (const statement of parsed.statements) {
206
- for (const [name, declares] of declaredNamesOf(statement))
192
+ for (const statement of body) {
193
+ const declaration = statement.type === "ExportNamedDeclaration"
194
+ ? statement.declaration
195
+ : statement.type === "ExportDefaultDeclaration"
196
+ ? defaultDeclaration(statement)
197
+ : statement;
198
+ if (declaration === null)
199
+ continue;
200
+ for (const [name, declares] of declaredNamesOf(declaration))
207
201
  locals.set(name, declares);
208
202
  }
209
203
  const sites = [];
210
- const site = (name, kind, declares, reexport) => {
211
- sites.push({ file, name, kind, declares, reexport });
204
+ const site = (node, name, kind, declares, reexport) => {
205
+ sites.push({ file, name, kind, declares, reexport, node });
212
206
  };
213
- for (const statement of parsed.statements) {
214
- if (ts.isExportDeclaration(statement)) {
215
- const reexport = statement.moduleSpecifier !== undefined;
216
- const clause = statement.exportClause;
217
- if (clause === undefined)
218
- site("*", "namespace", "other", true);
219
- else if (ts.isNamespaceExport(clause))
220
- site(clause.name.text, "namespace", "other", true);
221
- else {
222
- for (const element of clause.elements) {
223
- const name = nameOf(element.name);
207
+ for (const statement of body) {
208
+ switch (statement.type) {
209
+ case "ExportNamedDeclaration": {
210
+ const declaration = statement.declaration;
211
+ if (declaration !== null) {
212
+ for (const [name, declares] of declaredNamesOf(declaration)) {
213
+ site(declaration, name, "named", declares, false);
214
+ }
215
+ break;
216
+ }
217
+ const reexport = statement.source !== null;
218
+ for (const specifier of statement.specifiers) {
219
+ const name = nameOf(specifier.exported);
224
220
  if (name === null)
225
221
  continue;
226
- const local = nameOf(element.propertyName ?? element.name) ?? name;
222
+ const local = nameOf(specifier.local) ?? name;
227
223
  const declares = reexport ? "other" : (locals.get(local) ?? "other");
228
- site(name, name === "default" ? "default" : "named", declares, reexport);
224
+ site(specifier, name, name === "default" ? "default" : "named", declares, reexport);
229
225
  }
226
+ break;
230
227
  }
228
+ case "ExportDefaultDeclaration":
229
+ site(statement, "default", "default", defaultDeclares(statement, locals), false);
230
+ break;
231
+ case "ExportAllDeclaration":
232
+ site(statement, nameOf(statement.exported) ?? "*", "namespace", "other", true);
233
+ break;
234
+ default:
235
+ break;
231
236
  }
232
- else if (ts.isExportAssignment(statement)) {
233
- // `export = x` is a CommonJS surface, not a module's.
234
- if (statement.isExportEquals === true)
235
- continue;
236
- const declares = ts.isIdentifier(statement.expression)
237
- ? (locals.get(statement.expression.text) ?? "expression")
238
- : "expression";
239
- site("default", "default", declares, false);
237
+ }
238
+ return sites;
239
+ };
240
+ const IDLE = { file: "", edges: [], memberSites: [] };
241
+ let reading = IDLE;
242
+ const edge = (node, specifier, form, bindings) => {
243
+ reading.edges.push({ specifier, form, node, bindings });
244
+ };
245
+ // The members written in a declaration, under that declaration's name and
246
+ // kind, each at its key.
247
+ const declared = (declaration, declares, members) => {
248
+ for (const member of members) {
249
+ const key = keyOf(member);
250
+ if (key === null)
251
+ continue;
252
+ const name = nameOf(key);
253
+ if (name !== null) {
254
+ reading.memberSites.push({
255
+ file: reading.file,
256
+ subject: "members",
257
+ name,
258
+ in: declaration,
259
+ declares,
260
+ node: key,
261
+ });
240
262
  }
241
- else if (hasModifier(statement, ts.SyntaxKind.ExportKeyword)) {
242
- const isDefault = hasModifier(statement, ts.SyntaxKind.DefaultKeyword);
243
- const declared = declaredNamesOf(statement);
244
- if (isDefault) {
245
- // `export default function () {}` has no name to look up.
246
- const declares = declared[0]?.[1] ??
247
- (ts.isFunctionDeclaration(statement)
248
- ? "function"
249
- : ts.isClassDeclaration(statement)
250
- ? "class"
251
- : "other");
252
- site("default", "default", declares, false);
253
- }
254
- else {
255
- for (const [name, declares] of declared)
256
- site(name, "named", declares, false);
257
- }
263
+ }
264
+ };
265
+ // Every form that names a module is an edge, in source order. A computed
266
+ // `import(expr)` or `require(expr)` is not a fact a static policy can speak
267
+ // about.
268
+ const walker = new Visitor({
269
+ ImportDeclaration(node) {
270
+ // A side-effect import carries no bindings but is still an edge — the
271
+ // `import "server-only"` form a regex cannot see.
272
+ const bindings = node.specifiers.flatMap((specifier) => {
273
+ const found = bindingOf(specifier);
274
+ return found === null ? [] : [found];
275
+ });
276
+ edge(node, node.source.value, "import", bindings);
277
+ },
278
+ ExportNamedDeclaration(node) {
279
+ if (node.source !== null)
280
+ edge(node, node.source.value, "export", reexportedBindings(node));
281
+ },
282
+ ExportAllDeclaration(node) {
283
+ edge(node, node.source.value, "export", wholeModule(node, nameOf(node.exported) ?? ""));
284
+ },
285
+ TSImportEqualsDeclaration(node) {
286
+ const reference = node.moduleReference;
287
+ if (reference.type === "TSExternalModuleReference") {
288
+ edge(node, reference.expression.value, "import-equals", wholeModule(node, node.id.name));
289
+ }
290
+ },
291
+ ImportExpression(node) {
292
+ if (node.source.type === "Literal" && typeof node.source.value === "string") {
293
+ edge(node, node.source.value, "import-expression", wholeModule(node, ""));
294
+ }
295
+ },
296
+ CallExpression(node) {
297
+ const required = requireSpecifierOf(node);
298
+ if (required !== null)
299
+ edge(node, required, "require", wholeModule(node, ""));
300
+ const callee = calleeNameOf(node.callee);
301
+ if (callee !== null) {
302
+ reading.memberSites.push({ file: reading.file, subject: "calls", name: callee, node });
303
+ }
304
+ },
305
+ TSTypeAliasDeclaration(node) {
306
+ declared(node.id.name, "type", literalsOf(node.typeAnnotation).flatMap((literal) => literal.members));
307
+ },
308
+ TSInterfaceDeclaration(node) {
309
+ declared(node.id.name, "interface", node.body.body);
310
+ },
311
+ // A named class only: an anonymous default class has no name for `in`, and
312
+ // a class expression is a value. `ClassDeclaration` is the visitor key, so a
313
+ // class expression is never handed in.
314
+ ClassDeclaration(node) {
315
+ if (node.id !== null)
316
+ declared(node.id.name, "class", node.body.body);
317
+ },
318
+ });
319
+ // Reads every fact out of one program, with the node each came from. Pure over
320
+ // the tree: oxlint's plugin calls it on the tree oxlint parsed, and
321
+ // `factsOfText` on the one oxc-parser did.
322
+ export const readProgram = (file, program) => {
323
+ const read = { file, edges: [], memberSites: [] };
324
+ reading = read;
325
+ // The walk starts at a root of this shape and reads only its `body`.
326
+ walker.visit({
327
+ type: "Program",
328
+ body: program.body,
329
+ sourceType: "module",
330
+ hashbang: null,
331
+ start: 0,
332
+ end: 0,
333
+ });
334
+ reading = IDLE;
335
+ return {
336
+ edges: read.edges,
337
+ memberSites: read.memberSites,
338
+ exportSites: exportSitesOf(file, program.body),
339
+ };
340
+ };
341
+ const langOf = (file) => (file.endsWith(".tsx") ? "tsx" : "ts");
342
+ // Raw transfer hands the tree over as bytes and builds the nodes here, rather
343
+ // than serialising it to JSON in Rust and parsing that back — about three
344
+ // times the throughput of the JSON path on a source tree of ordinary files,
345
+ // and the difference between this parser and the compiler API being faster
346
+ // or slower. The buffer it reads from is reused across parses and returned
347
+ // before the tree is, so nothing here outlives a parse. The option is not in
348
+ // the parser's declared type yet; it is read all the same. Where the platform
349
+ // cannot do it (32-bit, big-endian, a Node before 22), the JSON path is what
350
+ // runs, at the same answer.
351
+ const RAW_TRANSFER = {
352
+ experimentalRawTransfer: rawTransferSupported(),
353
+ preserveParens: false,
354
+ };
355
+ const stripNode = (carrying) => {
356
+ const { node: _node, ...fact } = carrying;
357
+ return fact;
358
+ };
359
+ // The facts with the nodes stripped: one entry per specifier, in the order
360
+ // first written, carrying every binding pulled across it.
361
+ export const sourceFactsOf = (read) => {
362
+ const specifiers = [];
363
+ const bindings = new Map();
364
+ for (const edge of read.edges) {
365
+ let found = bindings.get(edge.specifier);
366
+ if (found === undefined) {
367
+ found = [];
368
+ bindings.set(edge.specifier, found);
369
+ specifiers.push(edge.specifier);
370
+ }
371
+ // The local name goes with the binding: it is how a campaign's `syntax`
372
+ // term traces an identifier back to its import. A form that binds no name
373
+ // (`import("m")`, `require("m")`) carries none.
374
+ for (const { kind, local, symbol } of edge.bindings) {
375
+ found.push(local === "" ? { symbol, kind } : { symbol, kind, local });
258
376
  }
259
377
  }
260
- return sites;
378
+ return {
379
+ specifiers,
380
+ bindings,
381
+ memberSites: read.memberSites.map(stripNode),
382
+ exportSites: read.exportSites.map(stripNode),
383
+ };
384
+ };
385
+ // The parse alone, for a source that need not be on disk — the CLI reads
386
+ // every file through it, a source probe is checked through it at load, and the
387
+ // parity suite feeds both hosts the same snippet through it. Parentheses are
388
+ // not kept as nodes, which is the tree oxlint hands a plugin. A syntax error
389
+ // does not throw: oxc reports it and returns an empty program, so a file that
390
+ // does not parse contributes no facts — the same file no linter would visit.
391
+ export const factsOfText = (file, text) => {
392
+ const { program } = parseSync(file, text, { ...RAW_TRANSFER, lang: langOf(file) });
393
+ return sourceFactsOf(readProgram(file, program));
261
394
  };
262
395
  export const makeFactExtractorLive = () => ({ factsOf: factsOfText });
263
396
  //# sourceMappingURL=extractor.js.map