@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/build/esm/extractor.js
CHANGED
|
@@ -1,263 +1,396 @@
|
|
|
1
|
-
import
|
|
2
|
-
// The
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
const
|
|
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 ===
|
|
14
|
+
if (node === null)
|
|
9
15
|
return null;
|
|
10
|
-
if (
|
|
11
|
-
return node.
|
|
12
|
-
if (
|
|
13
|
-
return node.
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
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
|
|
48
|
-
const symbol = nameOf(
|
|
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
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
69
|
-
// property or method signature in a type
|
|
70
|
-
// class.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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 = (
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
|
202
|
-
//
|
|
203
|
-
|
|
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
|
|
206
|
-
|
|
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
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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(
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
|
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
|