Skip to content

Commit e67f129

Browse files
committed
add AST helper functions
1 parent f824cdb commit e67f129

File tree

1 file changed

+258
-0
lines changed
  • packages/nextjs/src/config/loaders

1 file changed

+258
-0
lines changed
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import type { ASTNode as ASTNodeType } from 'ast-types';
2+
import * as jscsTypes from 'jscodeshift';
3+
import { default as jscodeshiftDefault } from 'jscodeshift';
4+
5+
import { makeParser } from './parsers';
6+
7+
// In `jscodeshift`, the exports look like this:
8+
//
9+
// function core(...) { ... }
10+
// core.ABC = ...
11+
// core.XYZ = ...
12+
// module.exports = core
13+
//
14+
// In other words, when required/imported, the module is both a callable function and an object containing all sorts of
15+
// properties. Meanwhile, its TS export is a namespace continaing the types of all of the properties attached to `core`.
16+
// In order to use the types, we thus need to use `import *` syntax. But when we do that, Rollup only sees it as a
17+
// namespace, and will complain if we try to use it as a function. In order to get around this, we take advantage of the
18+
// fact that Rollup wraps imports in its own version of TS's `esModuleInterop` functions, aliasing the export to a
19+
// `default` property inside the export. (So, here, we basically end up with `core.default = core`.) When referenced
20+
// through that alias, `core` is correctly seen as callable by Rollup. Outside of a Rollup context, however, that
21+
// `default` alias doesn't exist. So, we try both and use whichever one is defined. (See
22+
// https://github.com/rollup/rollup/issues/1267.)
23+
const jscodeshiftNamespace = jscsTypes;
24+
const jscs = jscodeshiftDefault || jscodeshiftNamespace;
25+
26+
// For some reason, the `ASTNode` type from `jscodeshift` doesn't match up with the `ASTNode` type expected by
27+
// `Collection.filter` functions, which are also from `jscodeshift`, so... it should work? But in `jscodeshift` there
28+
// seems to be a mismatch between the `ASTNode` type from `ast-types` and the `ASTNode` type from `ast-types/lib/types`,
29+
// wherein the former is exported but filter functions require the latter. Needs more investigation. In the meantime, we
30+
// can use the one from `ast-types` directly.
31+
type ASTFilterFunction = (path: jscsTypes.ASTPath<ASTNodeType>) => boolean;
32+
33+
const { ExportSpecifier, Identifier, Node, VariableDeclaration, VariableDeclarator } = jscs;
34+
35+
/**
36+
* Create a filter for nodes representing Identifiers with the given name
37+
*
38+
* @param name The variable name to filter on
39+
* @returns A filter function with the name baked in
40+
*/
41+
function makeIdentifierFilter(name: string): ASTFilterFunction {
42+
const identifierFilter = function (nodePath: jscsTypes.ASTPath<ASTNodeType>): boolean {
43+
// Check that what we have is indeed an Identifier, and that the name matches
44+
//
45+
// Note: If we were being super precise about this, we'd also check the context in which the identifier is being
46+
// used, because there are some cases where we actually don't want to be renaming things (if the identifier is being
47+
// used to name a class property, for example). But the chances that someone is going to have a class property in a
48+
// nextjs page file with the same name as one of the canonical functions are slim to none, so for simplicity we can
49+
// stop filtering here. If this ever becomes a problem, more precise filter checks can be found in a comment at the
50+
// bottom of this file.
51+
return Identifier.check(nodePath.node) && nodePath.node.name === name;
52+
};
53+
54+
return identifierFilter;
55+
}
56+
57+
/**
58+
* Create a filter for nodes declaring variables with the given name
59+
*
60+
* @param name The variable name to filter on
61+
* @returns A filter function with the name baked in
62+
*/
63+
function makeDeclarationFilter(name: string): ASTFilterFunction {
64+
// Check for a structure of the form
65+
//
66+
// node: VariableDeclaration
67+
// \
68+
// declarations: VariableDeclarator[]
69+
// \
70+
// 0 : VariableDeclarator
71+
// \
72+
// id: Identifier
73+
// \
74+
// name: string
75+
//
76+
// where `name` matches the given name.
77+
const declarationFilter = function (path: jscsTypes.ASTPath<ASTNodeType>): boolean {
78+
return (
79+
VariableDeclaration.check(path.node) &&
80+
path.node.declarations.length === 1 &&
81+
VariableDeclarator.check(path.node.declarations[0]) &&
82+
Identifier.check(path.node.declarations[0].id) &&
83+
path.node.declarations[0].id.name === name
84+
);
85+
};
86+
87+
return declarationFilter;
88+
}
89+
90+
/**
91+
* Create a filter for nodes representing exports with the given name
92+
*
93+
* @param name The variable name to filter on
94+
* @returns A filter function with the name baked in
95+
*/
96+
function makeExportFilter(name: string): ASTFilterFunction {
97+
const exportFilter = function (path: jscsTypes.ASTPath<ASTNodeType>): boolean {
98+
return ExportSpecifier.check(path.node) && path.node.exported.name === name;
99+
};
100+
101+
return exportFilter;
102+
}
103+
104+
/**
105+
* Find all nodes of the given type with the given name
106+
*
107+
* @param ast The code, in AST form
108+
* @param nodeType The type of node for which to search
109+
* @param name The identifier which should be associated with the found nodes
110+
* @returns A collection of NodePaths pointing to any nodes which were found
111+
*/
112+
function findNodes(ast: jscsTypes.Collection, nodeType: string, name: string): jscsTypes.Collection {
113+
const filterFactories: { [key: string]: (name: string) => ASTFilterFunction } = {
114+
VariableDeclaration: makeDeclarationFilter,
115+
Identifier: makeIdentifierFilter,
116+
ExportSpecifier: makeExportFilter,
117+
};
118+
const filter = filterFactories[nodeType](name);
119+
return ast.find(Node).filter(filter);
120+
}
121+
122+
/**
123+
* Find all nodes which are representing Identifiers with the given name
124+
*
125+
* @param ast The code, in AST form
126+
* @param name The Identifier name to search for
127+
* @returns A collection of NodePaths pointing to any nodes which were found
128+
*/
129+
export function findIdentifiers(ast: jscsTypes.Collection, name: string): jscsTypes.Collection<jscsTypes.Identifier> {
130+
return findNodes(ast, 'Identifier', name);
131+
}
132+
133+
/**
134+
* Find all nodes which are declarations of variables with the given name
135+
*
136+
* @param ast The code, in AST form
137+
* @param name The variable name to search for
138+
* @returns A collection of NodePaths pointing to any nodes which were found
139+
*/
140+
export function findDeclarations(
141+
ast: jscsTypes.Collection,
142+
name: string,
143+
): jscsTypes.Collection<jscsTypes.VariableDeclaration> {
144+
return findNodes(ast, 'VariableDeclaration', name);
145+
}
146+
147+
/**
148+
* Find all nodes which are exports of variables with the given name
149+
*
150+
* @param ast The code, in AST form
151+
* @param name The variable name to search for
152+
* @returns A collection of NodePaths pointing to any nodes which were found
153+
*/
154+
export function findExports(ast: jscsTypes.Collection, name: string): jscsTypes.Collection<jscsTypes.ExportSpecifier> {
155+
return findNodes(ast, 'ExportSpecifier', name);
156+
}
157+
158+
/**
159+
* Remove comments from all nodes in the given AST.
160+
*
161+
* Note: Comments are not nodes in and of themselves, but are instead attached to the nodes above and below them.
162+
*
163+
* @param ast The code, in AST form
164+
*/
165+
export function removeComments(ast: jscsTypes.Collection): void {
166+
const nodesWithComments = ast.find(Node).filter(path => !!path.node.comments);
167+
nodesWithComments.forEach(path => (path.node.comments = null));
168+
}
169+
170+
/**
171+
* Create an AST based on the given code.
172+
*
173+
* @param code The code to convert to an AST.
174+
* @param isTS Flag indicating what parser to use.
175+
* @throws Parsing error if the code is unparsable
176+
* @returns The AST
177+
*/
178+
export function makeAST(code: string, isTS: boolean): jscsTypes.Collection {
179+
const parser = isTS ? makeParser('tsx') : makeParser('jsx');
180+
// If this errors, it will be caught in the calling function, where we know more information and can construct a
181+
// better warning message
182+
return jscs(code, { parser });
183+
}
184+
185+
// /**
186+
// * More precise version of `makeIdentifierFilter`, which accounts for context. See note in `makeIdentifierFilter` above.
187+
// */
188+
//
189+
//
190+
// const {
191+
// AssignmentExpression,
192+
// CallExpression,
193+
// ExportSpecifier,
194+
// FunctionDeclaration,
195+
// Identifier,
196+
// MemberExpression,
197+
// Node,
198+
// Property,
199+
// ReturnStatement,
200+
// VariableDeclaration,
201+
// VariableDeclarator,
202+
// } = jscs;
203+
//
204+
// function makeIdentifierFilter(name: string): ASTFilterFunction {
205+
// const identifierFilter = function (nodePath: jscsTypes.ASTPath<ASTNodeType>): boolean {
206+
// // Get the easy checks out of the way first
207+
// const isIdentifier = Identifier.check(nodePath.node);
208+
// const hasCorrectName = (nodePath.node as jscsTypes.Identifier).name === name;
209+
// if (!isIdentifier || !hasCorrectName) {
210+
// return false;
211+
// }
212+
//
213+
// const node = nodePath.node as jscsTypes.Identifier;
214+
// // If we know `node` is an Identifier, it's safe to assume that `nodePath.parent` exists, because only the root of
215+
// // the AST has no parent, and it's never an Identifier
216+
// const parentPath = nodePath.parent as jscsTypes.ASTPath;
217+
// const parent = parentPath.node;
218+
//
219+
// // Check that the identifier is being used in a valid context, one in which we do in fact want to replace it.
220+
// //
221+
// // Note: There are a million ways identifiers can be used - this is just a subset, but it should hit 99% of cases.
222+
// // If anyone every files an issue because we're doing an incomplete job of transforming their code, get a
223+
// // representative sample from them and throw it into https://astexplorer.net/ or
224+
// // https://rajasegar.github.io/ast-finder/ (making sure in either case to set the parser to `recast`) to figure out
225+
// // what to add below. (Find the `Identifier` node and note its parent's `type` value and the name of the key under
226+
// // which it lives.) Note that neither tool seems to be able to handle the `export` keyword for some reason, but for
227+
// // anything other than the case already included below, `ExportSpecifier` will be at least the grandparent; given
228+
// // that we only care about recognizing the parent, we can just remove `export` from the sample code and it won't
229+
// // make any difference to the part we care about.
230+
// //
231+
// // In all of the examples in the comments below, the identifer we're interested in is `someFunc`.
232+
// const contextIsValid =
233+
// // `export const someFunc = ...` or `const someFunc = ...` or `let someFunc`
234+
// (VariableDeclarator.check(parent) && parent.id === node) ||
235+
// // `export { someFunc }` or `export { someOtherFunc as someFunc }`
236+
// (ExportSpecifier.check(parent) && parent.exported === node) ||
237+
// // `export function someFunc() { ... }` or `function someFunc() { ... }`
238+
// (FunctionDeclaration.check(parent) && parent.id === node) ||
239+
// // `someFunc = ...`
240+
// (AssignmentExpression.check(parent) && parent.left === node) ||
241+
// // `someVariable = someFunc`
242+
// (AssignmentExpression.check(parent) && parent.right === node) ||
243+
// // `const someVariable = someFunc`
244+
// (VariableDeclarator.check(parent) && parent.init === node) ||
245+
// // `someFunc.someProperty`
246+
// (MemberExpression.check(parent) && parent.object === node) ||
247+
// // `{ someProperty: someFunc }`
248+
// (Property.check(parent) && parent.value === node) ||
249+
// // `someOtherFunc(someFunc)`
250+
// (CallExpression.check(parent) && parent.arguments.includes(node)) ||
251+
// // `return someFunc`
252+
// (ReturnStatement.check(parent) && parent.argument === node);
253+
//
254+
// return contextIsValid;
255+
// };
256+
//
257+
// return identifierFilter;
258+
// }

0 commit comments

Comments
 (0)