@goodbones/typescript 0.1.0-beta.1 → 0.1.0-beta.11
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.
- package/build/dts/extractor.d.ts +28 -1
- package/build/dts/extractor.d.ts.map +1 -1
- package/build/dts/index.d.ts +6 -3
- package/build/dts/index.d.ts.map +1 -1
- package/build/esm/extractor.js +353 -220
- package/build/esm/extractor.js.map +1 -1
- package/build/esm/index.js +7 -2
- package/build/esm/index.js.map +1 -1
- package/package.json +4 -4
- package/src/extractor.ts +465 -218
- package/src/index.ts +27 -3
package/src/extractor.ts
CHANGED
|
@@ -6,277 +6,524 @@ import type {
|
|
|
6
6
|
MemberSite,
|
|
7
7
|
SourceFacts,
|
|
8
8
|
} from "@goodbones/core";
|
|
9
|
-
import
|
|
9
|
+
import {
|
|
10
|
+
type BindingIdentifier,
|
|
11
|
+
type BindingPattern,
|
|
12
|
+
type BindingRestElement,
|
|
13
|
+
type CallExpression,
|
|
14
|
+
type ClassElement,
|
|
15
|
+
type Declaration,
|
|
16
|
+
type Directive,
|
|
17
|
+
type ExportDefaultDeclaration,
|
|
18
|
+
type ExportNamedDeclaration,
|
|
19
|
+
type Expression,
|
|
20
|
+
type ImportDeclarationSpecifier,
|
|
21
|
+
type ModuleExportName,
|
|
22
|
+
type Node,
|
|
23
|
+
type ParserOptions,
|
|
24
|
+
parseSync,
|
|
25
|
+
type Program,
|
|
26
|
+
type PropertyKey,
|
|
27
|
+
rawTransferSupported,
|
|
28
|
+
type Statement,
|
|
29
|
+
type TSModuleDeclaration,
|
|
30
|
+
type TSSignature,
|
|
31
|
+
type TSType,
|
|
32
|
+
type TSTypeLiteral,
|
|
33
|
+
Visitor,
|
|
34
|
+
} from "oxc-parser";
|
|
10
35
|
|
|
11
|
-
// The facts, read out of
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
36
|
+
// The facts, read out of one ESTree. oxc-parser emits this tree for the CLI
|
|
37
|
+
// and oxlint hands the same tree to a plugin, so one reader serves both hosts:
|
|
38
|
+
// `readProgram` walks a `Program` and returns every fact with the node it came
|
|
39
|
+
// from, the plugin reports at those nodes, and `factsOfText` parses then strips
|
|
40
|
+
// them. The parity suite in `@goodbones/oxlint` pins that the two parsers, at
|
|
41
|
+
// their paired versions, produce the tree this reader expects.
|
|
15
42
|
|
|
16
|
-
|
|
17
|
-
|
|
43
|
+
// A fact with the syntax node it was read from — what a host that reports at
|
|
44
|
+
// a position needs, and what the core, which is position-free by design, never
|
|
45
|
+
// sees. `SourceFacts` is this with the nodes stripped.
|
|
46
|
+
export type SyntaxNode = Node;
|
|
18
47
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
48
|
+
// The syntactic form an edge was written in. Only an `import` declaration can
|
|
49
|
+
// be rewritten by an `exports` fix; the rest are reported and left as written.
|
|
50
|
+
export type EdgeForm = "import" | "export" | "import-equals" | "import-expression" | "require";
|
|
51
|
+
|
|
52
|
+
// One binding, with the node it was declared at and the local name it was
|
|
53
|
+
// bound to — the name a rewrite of the declaration has to preserve.
|
|
54
|
+
export type ReadBinding = Binding & { readonly node: SyntaxNode; readonly local: string };
|
|
55
|
+
|
|
56
|
+
// One place in the source that names a module. The same specifier written
|
|
57
|
+
// twice is two edges here and one in `SourceFacts.specifiers`.
|
|
58
|
+
export type ReadEdge = {
|
|
59
|
+
readonly specifier: string;
|
|
60
|
+
readonly form: EdgeForm;
|
|
61
|
+
readonly node: SyntaxNode;
|
|
62
|
+
readonly bindings: ReadonlyArray<ReadBinding>;
|
|
24
63
|
};
|
|
25
64
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
else {
|
|
34
|
-
for (const element of bindings.elements) {
|
|
35
|
-
const symbol = nameOf(element.propertyName ?? element.name);
|
|
36
|
-
if (symbol !== null) found.push({ symbol, kind: "named" });
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return found;
|
|
65
|
+
export type ReadMemberSite = MemberSite & { readonly node: SyntaxNode };
|
|
66
|
+
export type ReadExportSite = ExportSite & { readonly node: SyntaxNode };
|
|
67
|
+
|
|
68
|
+
export type ReadFacts = {
|
|
69
|
+
readonly edges: ReadonlyArray<ReadEdge>;
|
|
70
|
+
readonly memberSites: ReadonlyArray<ReadMemberSite>;
|
|
71
|
+
readonly exportSites: ReadonlyArray<ReadExportSite>;
|
|
41
72
|
};
|
|
42
73
|
|
|
43
74
|
// The whole module, as one binding. `export * from "m"`, `export * as ns from
|
|
44
75
|
// "m"`, `import x = require("m")`, `import("m")` and `require("m")` all carry
|
|
45
76
|
// every export of `m` at once, exactly as `import * as ns` does — and are the
|
|
46
77
|
// same way around a rule about a name. A side-effect import carries nothing.
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return found;
|
|
78
|
+
const wholeModule = (node: SyntaxNode, local: string): ReadonlyArray<ReadBinding> => [
|
|
79
|
+
{ symbol: "*", kind: "namespace", node, local },
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
// The name written at a key or a module export name: an identifier, or a
|
|
83
|
+
// string literal (`import { "a-b" as ab }`, `"c-d": number`). A numeric key,
|
|
84
|
+
// a private `#name` and a computed expression are not names a rule can speak
|
|
85
|
+
// about.
|
|
86
|
+
const nameOf = (node: PropertyKey | ModuleExportName | BindingIdentifier | null): string | null => {
|
|
87
|
+
if (node === null) return null;
|
|
88
|
+
if (node.type === "Identifier") return node.name;
|
|
89
|
+
if (node.type === "Literal" && typeof node.value === "string") return node.value;
|
|
90
|
+
return null;
|
|
61
91
|
};
|
|
62
92
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
93
|
+
const bindingOf = (specifier: ImportDeclarationSpecifier): ReadBinding | null => {
|
|
94
|
+
const local = specifier.local.name;
|
|
95
|
+
switch (specifier.type) {
|
|
96
|
+
case "ImportSpecifier": {
|
|
97
|
+
const symbol = nameOf(specifier.imported);
|
|
98
|
+
return symbol === null ? null : { symbol, kind: "named", node: specifier, local };
|
|
99
|
+
}
|
|
100
|
+
case "ImportDefaultSpecifier":
|
|
101
|
+
return { symbol: "default", kind: "default", node: specifier, local };
|
|
102
|
+
case "ImportNamespaceSpecifier":
|
|
103
|
+
return { symbol: "*", kind: "namespace", node: specifier, local };
|
|
71
104
|
}
|
|
72
|
-
if (ts.isParenthesizedTypeNode(node)) return literalsOf(node.type);
|
|
73
|
-
return [];
|
|
74
105
|
};
|
|
75
106
|
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
ts.isMethodSignature(member) ||
|
|
84
|
-
ts.isPropertyDeclaration(member) ||
|
|
85
|
-
ts.isMethodDeclaration(member) ||
|
|
86
|
-
ts.isGetAccessorDeclaration(member) ||
|
|
87
|
-
ts.isSetAccessorDeclaration(member);
|
|
88
|
-
|
|
89
|
-
const calleeNameOf = (expression: ts.LeftHandSideExpression): string | null => {
|
|
90
|
-
if (ts.isIdentifier(expression)) return expression.text;
|
|
91
|
-
if (ts.isPropertyAccessExpression(expression) && ts.isIdentifier(expression.name)) {
|
|
92
|
-
return expression.name.text;
|
|
107
|
+
// `export { a as b } from "m"` — `local` is the name in the source module, so
|
|
108
|
+
// it is the one the policy is about.
|
|
109
|
+
const reexportedBindings = (node: ExportNamedDeclaration): ReadonlyArray<ReadBinding> => {
|
|
110
|
+
const found: Array<ReadBinding> = [];
|
|
111
|
+
for (const specifier of node.specifiers) {
|
|
112
|
+
const symbol = nameOf(specifier.local);
|
|
113
|
+
if (symbol !== null) found.push({ symbol, kind: "named", node: specifier, local: symbol });
|
|
93
114
|
}
|
|
94
|
-
return
|
|
115
|
+
return found;
|
|
95
116
|
};
|
|
96
117
|
|
|
97
|
-
// The
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
118
|
+
// The type literals written in a type, through intersections and unions:
|
|
119
|
+
// `type Port = Base & ({ a(): void } | { b(): void })` declares `a` and `b`. A
|
|
120
|
+
// reference is not followed — `Base`'s members are declared where `Base` is,
|
|
121
|
+
// and are reported there under its own name. The parenthesised form is read
|
|
122
|
+
// too, for a tree parsed with `preserveParens`; oxlint's and `factsOfText`'s
|
|
123
|
+
// carry none.
|
|
124
|
+
const literalsOf = (node: TSType): ReadonlyArray<TSTypeLiteral> => {
|
|
125
|
+
switch (node.type) {
|
|
126
|
+
case "TSTypeLiteral":
|
|
127
|
+
return [node];
|
|
128
|
+
case "TSIntersectionType":
|
|
129
|
+
case "TSUnionType":
|
|
130
|
+
return node.types.flatMap(literalsOf);
|
|
131
|
+
case "TSParenthesizedType":
|
|
132
|
+
return literalsOf(node.typeAnnotation);
|
|
133
|
+
default:
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
};
|
|
106
137
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
138
|
+
// The key of a member shape that carries a name a vocabulary rule can speak
|
|
139
|
+
// about: a property or method signature in a type; a property, method,
|
|
140
|
+
// accessor or auto-accessor in a class. A computed key is no name, and neither
|
|
141
|
+
// is an index, call or construct signature, a static block, or a constructor.
|
|
142
|
+
const keyOf = (member: TSSignature | ClassElement): PropertyKey | null => {
|
|
143
|
+
switch (member.type) {
|
|
144
|
+
case "TSPropertySignature":
|
|
145
|
+
case "TSMethodSignature":
|
|
146
|
+
case "PropertyDefinition":
|
|
147
|
+
case "TSAbstractPropertyDefinition":
|
|
148
|
+
case "AccessorProperty":
|
|
149
|
+
case "TSAbstractAccessorProperty":
|
|
150
|
+
return member.computed ? null : member.key;
|
|
151
|
+
case "MethodDefinition":
|
|
152
|
+
case "TSAbstractMethodDefinition":
|
|
153
|
+
return member.computed || member.kind === "constructor" ? null : member.key;
|
|
154
|
+
default:
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
115
158
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
): void => {
|
|
125
|
-
for (const member of members) {
|
|
126
|
-
if (!isNamedMember(member)) continue;
|
|
127
|
-
if (ts.isComputedPropertyName(member.name) || ts.isPrivateIdentifier(member.name)) continue;
|
|
128
|
-
const name = nameOf(member.name);
|
|
129
|
-
if (name !== null) {
|
|
130
|
-
memberSites.push({ file, subject: "members", name, in: declaration, declares });
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
};
|
|
159
|
+
// The name a call is made by: `f()` and `x.f()` are both `f`. `x[f]()` and
|
|
160
|
+
// `x["f"]()` are not — a computed property is not a name a vocabulary rule can
|
|
161
|
+
// speak about — and neither is a private `x.#f()`.
|
|
162
|
+
const calleeNameOf = (callee: Expression): string | null => {
|
|
163
|
+
if (callee.type === "Identifier") return callee.name;
|
|
164
|
+
if (callee.type === "MemberExpression" && !callee.computed) return nameOf(callee.property);
|
|
165
|
+
return null;
|
|
166
|
+
};
|
|
134
167
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
} else if (
|
|
141
|
-
ts.isExportDeclaration(node) &&
|
|
142
|
-
node.moduleSpecifier !== undefined &&
|
|
143
|
-
ts.isStringLiteral(node.moduleSpecifier)
|
|
144
|
-
) {
|
|
145
|
-
record(node.moduleSpecifier.text, bindingsOfExportClause(node.exportClause));
|
|
146
|
-
} else if (
|
|
147
|
-
ts.isImportEqualsDeclaration(node) &&
|
|
148
|
-
ts.isExternalModuleReference(node.moduleReference) &&
|
|
149
|
-
ts.isStringLiteral(node.moduleReference.expression)
|
|
150
|
-
) {
|
|
151
|
-
record(node.moduleReference.expression.text, WHOLE_MODULE);
|
|
152
|
-
} else if (ts.isCallExpression(node)) {
|
|
153
|
-
const [first] = node.arguments;
|
|
154
|
-
const isModuleCall =
|
|
155
|
-
node.expression.kind === ts.SyntaxKind.ImportKeyword ||
|
|
156
|
-
(ts.isIdentifier(node.expression) && node.expression.text === "require");
|
|
157
|
-
if (isModuleCall && first !== undefined && ts.isStringLiteral(first)) {
|
|
158
|
-
record(first.text, WHOLE_MODULE);
|
|
159
|
-
}
|
|
168
|
+
const requireSpecifierOf = (node: CallExpression): string | null => {
|
|
169
|
+
if (node.callee.type !== "Identifier" || node.callee.name !== "require") return null;
|
|
170
|
+
const [first] = node.arguments;
|
|
171
|
+
return first?.type === "Literal" && typeof first.value === "string" ? first.value : null;
|
|
172
|
+
};
|
|
160
173
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
174
|
+
// The identifiers a binding pattern introduces.
|
|
175
|
+
const patternNames = (pattern: BindingPattern | BindingRestElement): ReadonlyArray<string> => {
|
|
176
|
+
switch (pattern.type) {
|
|
177
|
+
case "Identifier":
|
|
178
|
+
return [pattern.name];
|
|
179
|
+
case "ObjectPattern":
|
|
180
|
+
return pattern.properties.flatMap((property) =>
|
|
181
|
+
patternNames(property.type === "Property" ? property.value : property.argument),
|
|
168
182
|
);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
ts.forEachChild(node, visit);
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
visit(parsed);
|
|
182
|
-
|
|
183
|
-
return { specifiers, bindings, memberSites, exportSites: exportSitesOf(file, parsed) };
|
|
183
|
+
case "ArrayPattern":
|
|
184
|
+
return pattern.elements.flatMap((element) => (element === null ? [] : patternNames(element)));
|
|
185
|
+
case "AssignmentPattern":
|
|
186
|
+
return patternNames(pattern.left);
|
|
187
|
+
case "RestElement":
|
|
188
|
+
return patternNames(pattern.argument);
|
|
189
|
+
}
|
|
184
190
|
};
|
|
185
191
|
|
|
186
|
-
|
|
187
|
-
|
|
192
|
+
// `namespace A.B {}` declares `A`; `declare module "m" {}` declares no name a
|
|
193
|
+
// module's surface can carry.
|
|
194
|
+
const moduleNameOf = (node: TSModuleDeclaration): string | null => {
|
|
195
|
+
const id = node.id;
|
|
196
|
+
if (id.type !== "TSQualifiedName") return nameOf(id);
|
|
197
|
+
let left = id.left;
|
|
198
|
+
while (left.type === "TSQualifiedName") left = left.left;
|
|
199
|
+
return left.type === "Identifier" ? left.name : null;
|
|
200
|
+
};
|
|
188
201
|
|
|
189
202
|
// The names a declaration statement introduces, with what it declares them as.
|
|
190
203
|
// A destructuring pattern introduces names too, but not ones a surface rule
|
|
191
204
|
// judges by declaration kind; they read as `variable` like the rest.
|
|
192
|
-
const declaredNamesOf = (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
205
|
+
const declaredNamesOf = (
|
|
206
|
+
declaration: Directive | Statement,
|
|
207
|
+
): ReadonlyArray<readonly [string, DeclarationKind]> => {
|
|
208
|
+
const named = (
|
|
209
|
+
name: string | null,
|
|
210
|
+
declares: DeclarationKind,
|
|
211
|
+
): ReadonlyArray<readonly [string, DeclarationKind]> => (name === null ? [] : [[name, declares]]);
|
|
212
|
+
switch (declaration.type) {
|
|
213
|
+
case "VariableDeclaration":
|
|
214
|
+
return declaration.declarations.flatMap((one) =>
|
|
215
|
+
patternNames(one.id).map((name) => [name, "variable"] as const),
|
|
216
|
+
);
|
|
217
|
+
case "FunctionDeclaration":
|
|
218
|
+
case "TSDeclareFunction":
|
|
219
|
+
return named(declaration.id?.name ?? null, "function");
|
|
220
|
+
case "ClassDeclaration":
|
|
221
|
+
return named(declaration.id?.name ?? null, "class");
|
|
222
|
+
case "TSTypeAliasDeclaration":
|
|
223
|
+
return named(declaration.id.name, "type");
|
|
224
|
+
case "TSInterfaceDeclaration":
|
|
225
|
+
return named(declaration.id.name, "interface");
|
|
226
|
+
case "TSEnumDeclaration":
|
|
227
|
+
return named(declaration.id.name, "enum");
|
|
228
|
+
case "TSModuleDeclaration":
|
|
229
|
+
return named(declaration.global ? declaration.id.name : moduleNameOf(declaration), "other");
|
|
230
|
+
default:
|
|
231
|
+
return [];
|
|
198
232
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// The declaration an `export default …` carries, when it is one — a function,
|
|
236
|
+
// class or interface, named or not — rather than an expression.
|
|
237
|
+
const defaultDeclaration = (node: ExportDefaultDeclaration): Declaration | null => {
|
|
238
|
+
const declaration = node.declaration;
|
|
239
|
+
switch (declaration.type) {
|
|
240
|
+
case "FunctionDeclaration":
|
|
241
|
+
case "TSDeclareFunction":
|
|
242
|
+
case "ClassDeclaration":
|
|
243
|
+
case "TSInterfaceDeclaration":
|
|
244
|
+
return declaration;
|
|
245
|
+
default:
|
|
246
|
+
return null;
|
|
211
247
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// What `export default …` declares: a function, class or interface by its
|
|
251
|
+
// shape, named or not; an identifier by the declaration it names in this
|
|
252
|
+
// file; any other expression as one.
|
|
253
|
+
const defaultDeclares = (
|
|
254
|
+
node: ExportDefaultDeclaration,
|
|
255
|
+
locals: ReadonlyMap<string, DeclarationKind>,
|
|
256
|
+
): DeclarationKind => {
|
|
257
|
+
const declaration = node.declaration;
|
|
258
|
+
switch (declaration.type) {
|
|
259
|
+
case "FunctionDeclaration":
|
|
260
|
+
case "TSDeclareFunction":
|
|
261
|
+
return "function";
|
|
262
|
+
case "ClassDeclaration":
|
|
263
|
+
return "class";
|
|
264
|
+
case "TSInterfaceDeclaration":
|
|
265
|
+
return "interface";
|
|
266
|
+
case "Identifier":
|
|
267
|
+
return locals.get(declaration.name) ?? "expression";
|
|
268
|
+
default:
|
|
269
|
+
return "expression";
|
|
217
270
|
}
|
|
218
|
-
return [];
|
|
219
271
|
};
|
|
220
272
|
|
|
221
|
-
// A file's surface: what its top-level statements export, in source order
|
|
222
|
-
//
|
|
223
|
-
|
|
273
|
+
// A file's surface: what its top-level statements export, in source order,
|
|
274
|
+
// read off the program body rather than by visiting export nodes — an `export`
|
|
275
|
+
// inside a namespace body is that namespace's, not the module's, and reading
|
|
276
|
+
// the top level directly is what says so without a parent pointer. `export =`
|
|
277
|
+
// is a CommonJS surface, not a module's, and is stepped over.
|
|
278
|
+
const exportSitesOf = (
|
|
279
|
+
file: string,
|
|
280
|
+
body: ReadonlyArray<Directive | Statement>,
|
|
281
|
+
): ReadonlyArray<ReadExportSite> => {
|
|
224
282
|
const locals = new Map<string, DeclarationKind>();
|
|
225
|
-
for (const statement of
|
|
226
|
-
|
|
283
|
+
for (const statement of body) {
|
|
284
|
+
const declaration =
|
|
285
|
+
statement.type === "ExportNamedDeclaration"
|
|
286
|
+
? statement.declaration
|
|
287
|
+
: statement.type === "ExportDefaultDeclaration"
|
|
288
|
+
? defaultDeclaration(statement)
|
|
289
|
+
: statement;
|
|
290
|
+
if (declaration === null) continue;
|
|
291
|
+
for (const [name, declares] of declaredNamesOf(declaration)) locals.set(name, declares);
|
|
227
292
|
}
|
|
228
293
|
|
|
229
|
-
const sites: Array<
|
|
294
|
+
const sites: Array<ReadExportSite> = [];
|
|
230
295
|
const site = (
|
|
296
|
+
node: SyntaxNode,
|
|
231
297
|
name: string,
|
|
232
298
|
kind: ExportSite["kind"],
|
|
233
299
|
declares: DeclarationKind,
|
|
234
300
|
reexport: boolean,
|
|
235
301
|
): void => {
|
|
236
|
-
sites.push({ file, name, kind, declares, reexport });
|
|
302
|
+
sites.push({ file, name, kind, declares, reexport, node });
|
|
237
303
|
};
|
|
238
304
|
|
|
239
|
-
for (const statement of
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
305
|
+
for (const statement of body) {
|
|
306
|
+
switch (statement.type) {
|
|
307
|
+
case "ExportNamedDeclaration": {
|
|
308
|
+
const declaration = statement.declaration;
|
|
309
|
+
if (declaration !== null) {
|
|
310
|
+
for (const [name, declares] of declaredNamesOf(declaration)) {
|
|
311
|
+
site(declaration, name, "named", declares, false);
|
|
312
|
+
}
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
const reexport = statement.source !== null;
|
|
316
|
+
for (const specifier of statement.specifiers) {
|
|
317
|
+
const name = nameOf(specifier.exported);
|
|
248
318
|
if (name === null) continue;
|
|
249
|
-
const local = nameOf(
|
|
319
|
+
const local = nameOf(specifier.local) ?? name;
|
|
250
320
|
const declares = reexport ? "other" : (locals.get(local) ?? "other");
|
|
251
|
-
site(name, name === "default" ? "default" : "named", declares, reexport);
|
|
321
|
+
site(specifier, name, name === "default" ? "default" : "named", declares, reexport);
|
|
252
322
|
}
|
|
323
|
+
break;
|
|
253
324
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const isDefault = hasModifier(statement, ts.SyntaxKind.DefaultKeyword);
|
|
263
|
-
const declared = declaredNamesOf(statement);
|
|
264
|
-
if (isDefault) {
|
|
265
|
-
// `export default function () {}` has no name to look up.
|
|
266
|
-
const declares =
|
|
267
|
-
declared[0]?.[1] ??
|
|
268
|
-
(ts.isFunctionDeclaration(statement)
|
|
269
|
-
? "function"
|
|
270
|
-
: ts.isClassDeclaration(statement)
|
|
271
|
-
? "class"
|
|
272
|
-
: "other");
|
|
273
|
-
site("default", "default", declares, false);
|
|
274
|
-
} else {
|
|
275
|
-
for (const [name, declares] of declared) site(name, "named", declares, false);
|
|
276
|
-
}
|
|
325
|
+
case "ExportDefaultDeclaration":
|
|
326
|
+
site(statement, "default", "default", defaultDeclares(statement, locals), false);
|
|
327
|
+
break;
|
|
328
|
+
case "ExportAllDeclaration":
|
|
329
|
+
site(statement, nameOf(statement.exported) ?? "*", "namespace", "other", true);
|
|
330
|
+
break;
|
|
331
|
+
default:
|
|
332
|
+
break;
|
|
277
333
|
}
|
|
278
334
|
}
|
|
279
335
|
return sites;
|
|
280
336
|
};
|
|
281
337
|
|
|
338
|
+
// What the reader takes: the statements of one program. oxlint's `Program`
|
|
339
|
+
// and oxc-parser's differ in what else they carry, and the reader reads
|
|
340
|
+
// nothing else.
|
|
341
|
+
export type ProgramBody = Pick<Program, "body">;
|
|
342
|
+
|
|
343
|
+
// The read in progress: where the walk's handlers write. One `Visitor` is
|
|
344
|
+
// compiled once for the module — oxc-parser keeps every compiled visitor's
|
|
345
|
+
// handlers in a cache it never trims, so compiling one per call would retain
|
|
346
|
+
// the handlers and, through them, every tree ever read. `readProgram` is
|
|
347
|
+
// synchronous and no handler re-enters it, so one slot is enough.
|
|
348
|
+
type Reading = {
|
|
349
|
+
readonly file: string;
|
|
350
|
+
readonly edges: Array<ReadEdge>;
|
|
351
|
+
readonly memberSites: Array<ReadMemberSite>;
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const IDLE: Reading = { file: "", edges: [], memberSites: [] };
|
|
355
|
+
let reading: Reading = IDLE;
|
|
356
|
+
|
|
357
|
+
const edge = (
|
|
358
|
+
node: SyntaxNode,
|
|
359
|
+
specifier: string,
|
|
360
|
+
form: EdgeForm,
|
|
361
|
+
bindings: ReadonlyArray<ReadBinding>,
|
|
362
|
+
): void => {
|
|
363
|
+
reading.edges.push({ specifier, form, node, bindings });
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// The members written in a declaration, under that declaration's name and
|
|
367
|
+
// kind, each at its key.
|
|
368
|
+
const declared = (
|
|
369
|
+
declaration: string,
|
|
370
|
+
declares: DeclarationKind,
|
|
371
|
+
members: ReadonlyArray<TSSignature | ClassElement>,
|
|
372
|
+
): void => {
|
|
373
|
+
for (const member of members) {
|
|
374
|
+
const key = keyOf(member);
|
|
375
|
+
if (key === null) continue;
|
|
376
|
+
const name = nameOf(key);
|
|
377
|
+
if (name !== null) {
|
|
378
|
+
reading.memberSites.push({
|
|
379
|
+
file: reading.file,
|
|
380
|
+
subject: "members",
|
|
381
|
+
name,
|
|
382
|
+
in: declaration,
|
|
383
|
+
declares,
|
|
384
|
+
node: key,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// Every form that names a module is an edge, in source order. A computed
|
|
391
|
+
// `import(expr)` or `require(expr)` is not a fact a static policy can speak
|
|
392
|
+
// about.
|
|
393
|
+
const walker = new Visitor({
|
|
394
|
+
ImportDeclaration(node) {
|
|
395
|
+
// A side-effect import carries no bindings but is still an edge — the
|
|
396
|
+
// `import "server-only"` form a regex cannot see.
|
|
397
|
+
const bindings = node.specifiers.flatMap((specifier) => {
|
|
398
|
+
const found = bindingOf(specifier);
|
|
399
|
+
return found === null ? [] : [found];
|
|
400
|
+
});
|
|
401
|
+
edge(node, node.source.value, "import", bindings);
|
|
402
|
+
},
|
|
403
|
+
ExportNamedDeclaration(node) {
|
|
404
|
+
if (node.source !== null) edge(node, node.source.value, "export", reexportedBindings(node));
|
|
405
|
+
},
|
|
406
|
+
ExportAllDeclaration(node) {
|
|
407
|
+
edge(node, node.source.value, "export", wholeModule(node, nameOf(node.exported) ?? ""));
|
|
408
|
+
},
|
|
409
|
+
TSImportEqualsDeclaration(node) {
|
|
410
|
+
const reference = node.moduleReference;
|
|
411
|
+
if (reference.type === "TSExternalModuleReference") {
|
|
412
|
+
edge(node, reference.expression.value, "import-equals", wholeModule(node, node.id.name));
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
ImportExpression(node) {
|
|
416
|
+
if (node.source.type === "Literal" && typeof node.source.value === "string") {
|
|
417
|
+
edge(node, node.source.value, "import-expression", wholeModule(node, ""));
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
CallExpression(node) {
|
|
421
|
+
const required = requireSpecifierOf(node);
|
|
422
|
+
if (required !== null) edge(node, required, "require", wholeModule(node, ""));
|
|
423
|
+
|
|
424
|
+
const callee = calleeNameOf(node.callee);
|
|
425
|
+
if (callee !== null) {
|
|
426
|
+
reading.memberSites.push({ file: reading.file, subject: "calls", name: callee, node });
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
TSTypeAliasDeclaration(node) {
|
|
430
|
+
declared(
|
|
431
|
+
node.id.name,
|
|
432
|
+
"type",
|
|
433
|
+
literalsOf(node.typeAnnotation).flatMap((literal) => literal.members),
|
|
434
|
+
);
|
|
435
|
+
},
|
|
436
|
+
TSInterfaceDeclaration(node) {
|
|
437
|
+
declared(node.id.name, "interface", node.body.body);
|
|
438
|
+
},
|
|
439
|
+
// A named class only: an anonymous default class has no name for `in`, and
|
|
440
|
+
// a class expression is a value. `ClassDeclaration` is the visitor key, so a
|
|
441
|
+
// class expression is never handed in.
|
|
442
|
+
ClassDeclaration(node) {
|
|
443
|
+
if (node.id !== null) declared(node.id.name, "class", node.body.body);
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Reads every fact out of one program, with the node each came from. Pure over
|
|
448
|
+
// the tree: oxlint's plugin calls it on the tree oxlint parsed, and
|
|
449
|
+
// `factsOfText` on the one oxc-parser did.
|
|
450
|
+
export const readProgram = (file: string, program: ProgramBody): ReadFacts => {
|
|
451
|
+
const read: Reading = { file, edges: [], memberSites: [] };
|
|
452
|
+
reading = read;
|
|
453
|
+
// The walk starts at a root of this shape and reads only its `body`.
|
|
454
|
+
walker.visit({
|
|
455
|
+
type: "Program",
|
|
456
|
+
body: program.body,
|
|
457
|
+
sourceType: "module",
|
|
458
|
+
hashbang: null,
|
|
459
|
+
start: 0,
|
|
460
|
+
end: 0,
|
|
461
|
+
});
|
|
462
|
+
reading = IDLE;
|
|
463
|
+
return {
|
|
464
|
+
edges: read.edges,
|
|
465
|
+
memberSites: read.memberSites,
|
|
466
|
+
exportSites: exportSitesOf(file, program.body),
|
|
467
|
+
};
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const langOf = (file: string): "ts" | "tsx" => (file.endsWith(".tsx") ? "tsx" : "ts");
|
|
471
|
+
|
|
472
|
+
// Raw transfer hands the tree over as bytes and builds the nodes here, rather
|
|
473
|
+
// than serialising it to JSON in Rust and parsing that back — about three
|
|
474
|
+
// times the throughput of the JSON path on a source tree of ordinary files,
|
|
475
|
+
// and the difference between this parser and the compiler API being faster
|
|
476
|
+
// or slower. The buffer it reads from is reused across parses and returned
|
|
477
|
+
// before the tree is, so nothing here outlives a parse. The option is not in
|
|
478
|
+
// the parser's declared type yet; it is read all the same. Where the platform
|
|
479
|
+
// cannot do it (32-bit, big-endian, a Node before 22), the JSON path is what
|
|
480
|
+
// runs, at the same answer.
|
|
481
|
+
const RAW_TRANSFER: ParserOptions & { readonly experimentalRawTransfer: boolean } = {
|
|
482
|
+
experimentalRawTransfer: rawTransferSupported(),
|
|
483
|
+
preserveParens: false,
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const stripNode = <T extends { readonly node: SyntaxNode }>(carrying: T): Omit<T, "node"> => {
|
|
487
|
+
const { node: _node, ...fact } = carrying;
|
|
488
|
+
return fact;
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
// The facts with the nodes stripped: one entry per specifier, in the order
|
|
492
|
+
// first written, carrying every binding pulled across it.
|
|
493
|
+
export const sourceFactsOf = (read: ReadFacts): SourceFacts => {
|
|
494
|
+
const specifiers: Array<string> = [];
|
|
495
|
+
const bindings = new Map<string, Array<Binding>>();
|
|
496
|
+
for (const edge of read.edges) {
|
|
497
|
+
let found = bindings.get(edge.specifier);
|
|
498
|
+
if (found === undefined) {
|
|
499
|
+
found = [];
|
|
500
|
+
bindings.set(edge.specifier, found);
|
|
501
|
+
specifiers.push(edge.specifier);
|
|
502
|
+
}
|
|
503
|
+
// The local name goes with the binding: it is how a campaign's `syntax`
|
|
504
|
+
// term traces an identifier back to its import. A form that binds no name
|
|
505
|
+
// (`import("m")`, `require("m")`) carries none.
|
|
506
|
+
for (const { kind, local, symbol } of edge.bindings) {
|
|
507
|
+
found.push(local === "" ? { symbol, kind } : { symbol, kind, local });
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
specifiers,
|
|
512
|
+
bindings,
|
|
513
|
+
memberSites: read.memberSites.map(stripNode),
|
|
514
|
+
exportSites: read.exportSites.map(stripNode),
|
|
515
|
+
};
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
// The parse alone, for a source that need not be on disk — the CLI reads
|
|
519
|
+
// every file through it, a source probe is checked through it at load, and the
|
|
520
|
+
// parity suite feeds both hosts the same snippet through it. Parentheses are
|
|
521
|
+
// not kept as nodes, which is the tree oxlint hands a plugin. A syntax error
|
|
522
|
+
// does not throw: oxc reports it and returns an empty program, so a file that
|
|
523
|
+
// does not parse contributes no facts — the same file no linter would visit.
|
|
524
|
+
export const factsOfText = (file: string, text: string): SourceFacts => {
|
|
525
|
+
const { program } = parseSync(file, text, { ...RAW_TRANSFER, lang: langOf(file) });
|
|
526
|
+
return sourceFactsOf(readProgram(file, program));
|
|
527
|
+
};
|
|
528
|
+
|
|
282
529
|
export const makeFactExtractorLive = (): FactExtractor => ({ factsOf: factsOfText });
|