Last active
March 3, 2021 20:08
-
-
Save a-laughlin/6decd6efd67231c84dc31a7c71bf38f6 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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