Skip to content

Commit 5b7c55f

Browse files
committed
add AST helper functions
1 parent 38bffbb commit 5b7c55f

File tree

1 file changed

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

1 file changed

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

0 commit comments

Comments
 (0)