Skip to content

Instantly share code, notes, and snippets.

@a-laughlin
Last active March 3, 2021 20:08
Show Gist options
  • Save a-laughlin/6decd6efd67231c84dc31a7c71bf38f6 to your computer and use it in GitHub Desktop.
Save a-laughlin/6decd6efd67231c84dc31a7c71bf38f6 to your computer and use it in GitHub Desktop.
// purpose: tools to make replacements from deeply nested objects to imports for large codebase migrations
// usage:
// https://astexplorer.net/#/gist/b8ff1963328449b8063253147c04a462/111b0113648c8f800e8fac56d54e49006c3c2c2b
const isObjectPath = p =>
p.type === 'MemberExpression' ||
p.parentPath.type === 'MemberExpression' ||
(p.parentPath.type === 'CallExpression' && p.parentPath.parentPath.type === 'MemberExpression');
// loops over a path, starting at base object, up to the first function call
const eachPath = fn => p => {
if (!isObjectPath(p) && p.type !== 'ThisExpression' && p.type !== 'Identifier') {
console.error(`must call eachPath within an existing object path or single identifier name (that may or may not represent an object in your code)`, p);
return;
}
if (p.parentPath.node.property === p.node) p = p.parentPath; // on property identifier. Switch to object branch.
// walk to path root (but AST leaf) of App in App.foo.bar or App.foo.bar();
while (p.type !== 'Identifier' && p.type !== 'ThisExpression') {
if (p.node.object) p = p.get('object');
else if (p.node.callee) p = p.get('callee');
else return console.error(`Hit non-identifier AST member expression leaf. Code author missed didn't handle this case yet.`, p);
}
fn(p);
// remove the CallExpression Case since we don't need to know what objects return yet
while (p.parentPath.type === 'MemberExpression' /*||p.parentPath.type==='CallExpression'*/) {
// walk up from App to bar in "App.foo.bar" or "App.foo.bar()";
fn((p = p.parentPath));
}
};
// given path of LIQUID.stores.SessionStorageStore
// returns {arr:["LIQUID", "stores", "SessionStorageStore"],root:"LIQUID",leaf:"SessionStorageStore"}
const indexPath = initialPath => {
const arr = [];
const pathArr = [];
eachPath(p => {
const {node, type} = p;
pathArr[pathArr.length] = p;
if (node.name) arr[arr.length] = node.name;
// Identifier
else if (node.property) arr[arr.length] = node.property.name /*foo.bar*/ || node.property.value /*foo['bar']*/;
// CallExpression or MemberExpression with property.
else if (type === 'ThisExpression') arr[arr.length] = 'this';
else if (type === 'CallExpression' || type === 'MemberExpression') null;
// do nothing since no property name in foo.bar().baz case.
else console.error(`eachPathName hit unknown node shape`, p);
})(initialPath);
return {
arr,
root: arr[0],
leaf: arr[arr.length - 1],
rootPath: pathArr[0],
leafPath: pathArr[pathArr.length - 1],
pathArr,
leafParentPath: pathArr[pathArr.length - 1].parentPath,
};
};
const getReferenceContext=(p)=>{
let pp=p.parentPath;
while(true){
if(pp.type==='ObjectProperty') return pp;
if(pp.type==='ArrayExpression') return pp;
if(pp.type==='AssignmentExpression') return pp;
if(pp.type==='VariableDeclarator') return pp;
if(pp.type==='ExpressionStatement') return pp;
if(pp.type==='Program'){
console.log('error context',p);
throw new Error(`Recursed to parent program in getReferenceContext. Add a type case for this context.`);
}
pp=pp.parentPath
}
}
const replaceWithExport=(toReplace,left,right,ast)=>{
const replacement = ast(`export const ${left} = 1;`);
replacement.declaration.declarations[0].init=right
replacement.trailingComments=toReplace.trailingComments
replacement.leadingComments=toReplace.leadingComments
toReplace.replaceWith(replacement);
}
const replaceWithExpression=(toReplace,replacement,ast)=>{
//console.log('replaceWithExpression')
console.log('1',toReplace.type,toReplace.toString(),replacement)
replacement = ast(replacement);
replacement.leadingComments=toReplace.leadingComments
replacement.trailingComments=toReplace.trailingComments
toReplace.replaceWith(replacement);
}
const getReplaceReference=({
shouldSkip=(paramsObj,pathIndex)=>paramsObj.origArray.length!==pathIndex.pathArr.length-1,
getNext=(paramsObj,pathIndex)=>pathIndex.arr[paramsObj.origArray.length],
getToReplace=(paramsObj,pathIndex)=>pathIndex.pathArr[paramsObj.origArray.length]
}={})=>( paramsObj, pathIndex, ast, importsSet )=>{
let {orig,origArray,next,importDir,importFileName}=paramsObj;
const {leafPath,arr,pathArr}=pathIndex;
next=getNext(paramsObj,pathIndex);
const contextPath = getReferenceContext(leafPath);
// replace Assignment
if('AssignmentExpression'===contextPath.type){
if (shouldSkip(paramsObj,pathIndex)) return;
if(contextPath.node.left===leafPath.node){ // <foo> = bar
replaceWithExport(contextPath.parentPath,next,contextPath.node.right,ast);
} else{ // foo = <bar.baz>
// no need to replace on single word import
if(origArray.length>1) replaceWithExpression(getToReplace(paramsObj,pathIndex),next,ast);
if(importDir) importsSet.add(`import {${next}} from '${importDir}/${importFileName}';`);
}
return;
}
if('ObjectProperty'===contextPath.type){
if(contextPath.node.id===leafPath.node){ // <foo> = bar
console.log('ObjectProperty unhandled case in replaceVariableReference')
} else{ // foo = <bar.baz>
// no need to replace on single word import
if(origArray.length>1) replaceWithExpression(getToReplace(paramsObj,pathIndex),next,ast);
if(importDir) importsSet.add(`import {${next}} from '${importDir}/${importFileName}';`);
}
return;
}
if('ArrayExpression'===contextPath.type){
if(origArray.length>1) replaceWithExpression(getToReplace(paramsObj,pathIndex),next,ast);
if(importDir) importsSet.add(`import {${next}} from '${importDir}/${importFileName}';`);
return;
}
if('VariableDeclarator'===contextPath.type){
if (shouldSkip(paramsObj,pathIndex)) return;
if(contextPath.parentPath.node.declarations.length>1){
console.log(`manually fix unhandled case, VariableDeclarator among multiple`,contextPath.parentPath.getSource());
} else {
if (contextPath.node.id===leafPath.node){
replaceWithExport(contextPath.parentPath,next,contextPath.init,ast);
} else {
if(origArray.length>1) replaceWithExpression(getToReplace(paramsObj,pathIndex),next,ast);
if(importDir) importsSet.add(`import {${next}} from '${importDir}/${importFileName}';`);
}
}
return;
}
if(origArray.length>1) replaceWithExpression(getToReplace(paramsObj,pathIndex),next,ast);
if(importDir) importsSet.add(`import {${next}} from '${importDir}/${importFileName}';`);
}
replaceVariableReference=getReplaceReference();
const replaceStaticReference=getReplaceReference({
shouldSkip:(paramsObj,pathIndex)=>paramsObj.origArray.length!==pathIndex.pathArr.length,
getNext:(paramsObj,pathIndex)=>paramsObj.next,
getToReplace:(paramsObj,pathIndex)=>pathIndex.pathArr[paramsObj.origArray.length-1]
});
const getReplaceWith=(
ast=()=>{/*babel.template.ast*/},
replacementsMap=new Map(),
importsSet=new Set()
)=>{
const addReplacement=(orig='App.translator.translate',next='translate',importDir='',importFileName='',modFn=replaceStaticReference)=>{
//'@shopping/globals/translator',
if(importFileName==='')importFileName=next;
const origArray = orig.split('.');
const identifier=origArray[origArray.length-1];
if(!replacementsMap.has(identifier)) replacementsMap.set(identifier,[]);
replacementsMap.get(identifier).push({orig,origArray,next,modFn,importDir,importFileName});
if (origArray[0]==='globalThis')return;
replacementsMap.get(identifier).push({orig:`globalThis.${orig}`,modFn,origArray:['globalThis',...origArray],next,importDir,importFileName});
};
const execReplacements=(programPath)=>{
programPath.traverse({
Identifier:(p)=>{
if (!replacementsMap.has(p.node.name)) return;
p.skip();
const pathIndex=indexPath(p);
// to prevent maximum call stack exceeded by importing $.
// pathIndex.arr.length check may be unnecessary if we have the correct checks for left/ride side of assignments in the file
if (pathIndex.arr.length===1 && pathIndex.root in pathIndex.rootPath.scope.bindings) return;
// have to loop over these, since we're indexing only by the substring to match on,
// not the whole string, since each identifier is a separate object.
replacementsMap.get(p.node.name).forEach(replacementObj=>{
const {orig,origArray,next,importString} = replacementObj;
let i=-1,L=origArray.length;
while(++i<L){
if (origArray[i]!==pathIndex.arr[i])return;
}
replacementObj.modFn(replacementObj,pathIndex,ast,importsSet);
});
}
});
}
return {addReplacement,execReplacements,importsSet};
}
module.exports={
getReplaceWith,
replaceVariableReference,
indexPath,
isObjectPath,
eachPath
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment