const generator = require("@babel/generator").default; const { clone, parseArgs, parseFile, reparse, saveAst, t, traverse } = require("./shared"); function getCaseStatements(casePath) { return casePath.get("consequent").filter(stmt => !stmt.isContinueStatement() && !stmt.isBreakStatement()).map(stmt => clone(stmt.node)); } function hasLexicalBindings(nodes) { return nodes.some((node) => t.isVariableDeclaration(node) && node.kind !== "var"); } function isSimpleDiscriminant(node) { return t.isIdentifier(node) || t.isLiteral(node); } function normalizeCaseConsequent(nodes) { const statements = nodes.map((node) => clone(node)); if (statements.length === 0) { return [t.continueStatement()]; } const last = statements[statements.length - 1]; if (t.isBreakStatement(last)) { statements.pop(); statements.push(t.continueStatement()); return statements; } if (t.isContinueStatement(last) || t.isReturnStatement(last) || t.isThrowStatement(last)) { return statements; } return null; } function buildIfChainFromSwitch(switchNode) { if (!isSimpleDiscriminant(switchNode.discriminant)) { return null; } let root = null; let current = null; let defaultBlock = null; for (const caseNode of switchNode.cases) { const normalized = normalizeCaseConsequent(caseNode.consequent); if (!normalized || hasLexicalBindings(normalized)) { return null; } if (caseNode.test === null) { defaultBlock = t.blockStatement(normalized); continue; } const test = t.logicalExpression( "&&", t.binaryExpression("===", clone(switchNode.discriminant), clone(caseNode.test)), t.booleanLiteral(true) ); const nextIf = t.ifStatement(test, t.blockStatement(normalized)); if (!root) { root = nextIf; } else { current.alternate = nextIf; } current = nextIf; } if (!root) { return null; } if (current && defaultBlock) { current.alternate = defaultBlock; } return root; } function replaceLoopSwitchWithIf(path) { const body = path.get("body"); if (!body.isBlockStatement()) { return false; } const statements = body.get("body"); if (statements.length !== 1 || !statements[0].isSwitchStatement()) { return false; } const ifChain = buildIfChainFromSwitch(statements[0].node); if (!ifChain) { return false; } body.replaceWith(t.blockStatement([ifChain])); return true; } const UNRESOLVED = Symbol("unresolved"); function getResolvedFunctionCallPath(functionPath) { if (functionPath.parentPath.isCallExpression() && functionPath.parentKey === "callee") { return functionPath.parentPath; } if ( functionPath.parentPath.isUnaryExpression() && functionPath.parentPath.parentPath && functionPath.parentPath.parentPath.isCallExpression() && functionPath.parentPath.parentKey === "callee" ) { return functionPath.parentPath.parentPath; } return null; } function resolveObjectPropertyFromBinding(objectPath, propertyValue, seenBindings = new Set()) { if (!objectPath || !objectPath.node || !objectPath.isIdentifier()) { return UNRESOLVED; } const binding = objectPath.scope.getBinding(objectPath.node.name); if (!binding || seenBindings.has(binding)) { return UNRESOLVED; } seenBindings.add(binding); const declaratorPath = getDeclaratorForBinding(binding); if (!declaratorPath) { seenBindings.delete(binding); return UNRESOLVED; } const initPath = declaratorPath.get("init"); if (!initPath.node) { seenBindings.delete(binding); return UNRESOLVED; } if (initPath.isIdentifier()) { const resolved = resolveObjectPropertyFromBinding(initPath, propertyValue, seenBindings); seenBindings.delete(binding); return resolved; } if (!initPath.isObjectExpression()) { seenBindings.delete(binding); return UNRESOLVED; } for (const propertyPath of initPath.get("properties")) { if (!propertyPath.isObjectProperty()) { continue; } const keyPath = propertyPath.get("key"); const key = propertyPath.node.computed ? resolveStatic(keyPath, seenBindings) : keyPath.isIdentifier() ? keyPath.node.name : resolveStatic(keyPath, seenBindings); if (key === UNRESOLVED || String(key) !== String(propertyValue)) { continue; } const resolved = resolveStatic(propertyPath.get("value"), seenBindings); seenBindings.delete(binding); return resolved; } seenBindings.delete(binding); return UNRESOLVED; } function resolveStatic(path, seenBindings = new Set()) { if (!path || !path.node) { return UNRESOLVED; } if (path.isStringLiteral() || path.isNumericLiteral() || path.isBooleanLiteral()) { return path.node.value; } if (path.isNullLiteral()) { return null; } if (path.isTemplateLiteral() && path.node.expressions.length === 0) { return path.node.quasis[0].value.cooked; } if (path.isIdentifier({ name: "undefined" })) { return undefined; } if (path.isArrayExpression()) { const values = []; for (const elementPath of path.get("elements")) { if (!elementPath.node) { values.push(undefined); continue; } const value = resolveStatic(elementPath, seenBindings); if (value === UNRESOLVED) { return UNRESOLVED; } values.push(value); } return values; } if (path.isObjectExpression()) { const out = Object.create(null); for (const propPath of path.get("properties")) { if (!propPath.isObjectProperty()) { return UNRESOLVED; } const keyPath = propPath.get("key"); const key = propPath.node.computed ? resolveStatic(keyPath, seenBindings) : keyPath.isIdentifier() ? keyPath.node.name : resolveStatic(keyPath, seenBindings); if (key === UNRESOLVED) { return UNRESOLVED; } const value = resolveStatic(propPath.get("value"), seenBindings); if (value === UNRESOLVED) { return UNRESOLVED; } out[key] = value; } return out; } if (path.isIdentifier()) { const binding = path.scope.getBinding(path.node.name); if (!binding || seenBindings.has(binding)) { return UNRESOLVED; } seenBindings.add(binding); let resolved = UNRESOLVED; if (binding.path.parentPath && binding.path.parentPath.isVariableDeclarator()) { resolved = resolveStatic(binding.path.parentPath.get("init"), seenBindings); } else if (binding.kind === "param" && binding.path.parentPath && binding.path.parentPath.isFunction()) { const callPath = getResolvedFunctionCallPath(binding.path.parentPath); if (callPath && typeof binding.path.key === "number") { resolved = resolveStatic(callPath.get(`arguments.${binding.path.key}`), seenBindings); } } seenBindings.delete(binding); if (resolved !== UNRESOLVED) { return resolved; } } if (path.isMemberExpression()) { const propertyValue = path.node.computed ? resolveStatic(path.get("property"), seenBindings) : path.get("property").isIdentifier() ? path.node.property.name : resolveStatic(path.get("property"), seenBindings); if (propertyValue === UNRESOLVED) { return UNRESOLVED; } const directPropertyValue = resolveObjectPropertyFromBinding(path.get("object"), propertyValue, seenBindings); if (directPropertyValue !== UNRESOLVED) { return directPropertyValue; } const objectValue = resolveStatic(path.get("object"), seenBindings); if (objectValue === UNRESOLVED) { return UNRESOLVED; } if (Array.isArray(objectValue)) { const index = Number(propertyValue); return Number.isInteger(index) ? objectValue[index] : UNRESOLVED; } if (objectValue && typeof objectValue === "object" && propertyValue in objectValue) { return objectValue[propertyValue]; } return UNRESOLVED; } if ( path.isCallExpression() && path.get("callee").isMemberExpression() && ( path.get("callee.property").isIdentifier({ name: "split" }) || path.get("callee.property").isStringLiteral({ value: "split" }) ) && path.get("arguments.0").isStringLiteral() ) { const source = resolveStatic(path.get("callee.object"), seenBindings); if (typeof source === "string") { return source.split(path.node.arguments[0].value); } } const evaluated = path.evaluate(); return evaluated.confident ? evaluated.value : UNRESOLVED; } function getLiteralValue(path) { const resolved = resolveStatic(path); if (typeof resolved === "string" || typeof resolved === "number") { return resolved; } return null; } function normalizeKey(node) { return generator(node, { compact: true, comments: false, jsescOption: { minimal: true } }).code; } function getCaseKey(casePath) { const value = getLiteralValue(casePath.get("test")); return value === null ? normalizeKey(casePath.node.test) : value; } function getOrderFromInit(initPath) { if (initPath.isArrayExpression()) { const values = []; for (const elementPath of initPath.get("elements")) { if (!elementPath.node) { return null; } const value = getLiteralValue(elementPath); values.push(value === null ? normalizeKey(elementPath.node) : value); } return values; } if ( initPath.isCallExpression() && initPath.get("callee").isMemberExpression() && ( initPath.get("callee.property").isIdentifier({ name: "split" }) || initPath.get("callee.property").isStringLiteral({ value: "split" }) ) && initPath.get("arguments.0").isStringLiteral() ) { const source = getLiteralValue(initPath.get("callee.object")); if (typeof source === "string") { return source.split(initPath.node.arguments[0].value); } } return null; } function getOrderFromResolvedValue(value) { if (!Array.isArray(value) || value.length === 0) { return null; } const order = []; for (const item of value) { if (typeof item === "string" || typeof item === "number") { order.push(item); continue; } return null; } return order; } function getDeclaratorForBinding(binding) { if (!binding || !binding.path) { return null; } if (binding.path.isVariableDeclarator()) { return binding.path; } if (binding.path.parentPath && binding.path.parentPath.isVariableDeclarator()) { return binding.path.parentPath; } return null; } function getOrderFromBinding(binding) { const declaratorPath = getDeclaratorForBinding(binding); if (!declaratorPath) { return null; } const initPath = declaratorPath.get("init"); if (!initPath.node) { return null; } return getOrderFromInit(initPath) || getOrderFromResolvedValue(resolveStatic(initPath)); } function getOrderFromSplitCallWithObjectTable(initPath, loopPath) { if ( !initPath.isCallExpression() || !initPath.get("callee").isMemberExpression() || !( initPath.get("callee.property").isIdentifier({ name: "split" }) || initPath.get("callee.property").isStringLiteral({ value: "split" }) ) || !initPath.get("arguments.0").isStringLiteral() ) { return null; } const sourcePath = initPath.get("callee.object"); if (!sourcePath.isMemberExpression() || !sourcePath.get("object").isIdentifier()) { return null; } const objectName = sourcePath.get("object").node.name; const propertyKey = sourcePath.node.computed ? getStaticPropertyKey(sourcePath.node.property) : getStaticPropertyKey(sourcePath.node.property); if (propertyKey === null) { return null; } const match = collectObjectPipeStringCandidates(loopPath, objectName).find((candidate) => candidate.key === propertyKey); return match ? match.order : null; } function getOrderFromDiscriminant(discriminantPath) { if (!discriminantPath.isMemberExpression()) { return null; } const objectPath = discriminantPath.get("object"); const directOrder = getOrderFromResolvedValue(resolveStatic(objectPath)); if (directOrder) { return directOrder; } if (!objectPath.isIdentifier()) { return null; } const binding = objectPath.scope.getBinding(objectPath.node.name); return getOrderFromBinding(binding); } function findNearbyOrderDeclaration(loopPath, maxSiblings = 3) { let siblingPath = loopPath.getPrevSibling(); for (let seen = 0; siblingPath && siblingPath.node && seen < maxSiblings; seen += 1) { if (!siblingPath.isVariableDeclaration()) { break; } for (const declaratorPath of siblingPath.get("declarations")) { if (!declaratorPath.node || !declaratorPath.get("init").node) { continue; } const order = getOrderFromInit(declaratorPath.get("init")) || getOrderFromSplitCallWithObjectTable(declaratorPath.get("init"), loopPath) || getOrderFromResolvedValue(resolveStatic(declaratorPath.get("init"))); if (order) { return { order, declarationPath: siblingPath }; } } siblingPath = siblingPath.getPrevSibling(); } return null; } function buildBlocksFromSwitchOrder(switchStmt, order) { if (!order) { return null; } const caseMap = new Map(); switchStmt.get("cases").forEach(casePath => { if (!casePath.node.test) { return; } caseMap.set(getCaseKey(casePath), getCaseStatements(casePath)); }); const blocks = []; order.forEach(key => { const stmts = caseMap.get(key); if (stmts) { blocks.push(...stmts); } }); return blocks.length > 0 ? blocks : null; } function getStaticPropertyKey(node) { if (t.isIdentifier(node)) { return node.name; } if (t.isStringLiteral(node) || t.isNumericLiteral(node)) { return String(node.value); } return null; } function resolveObjectAliasNames(loopPath, objectName) { const names = new Set(); let currentName = objectName; while (currentName) { if (names.has(currentName)) { break; } names.add(currentName); const binding = loopPath.scope.getBinding(currentName); const declaratorPath = getDeclaratorForBinding(binding); if (!declaratorPath || !declaratorPath.get("init").isIdentifier()) { break; } currentName = declaratorPath.get("init").node.name; } return names; } function splitPipeString(value) { if (typeof value !== "string" || !value.includes("|")) { return null; } const parts = value.split("|"); return parts.length > 1 ? parts : null; } function getPipeOrderFromValuePath(valuePath) { if (!valuePath || !valuePath.node) { return null; } if (valuePath.isStringLiteral()) { return splitPipeString(valuePath.node.value); } const resolved = resolveStatic(valuePath); return splitPipeString(resolved); } function getCaseKeys(switchStmt) { const keys = []; switchStmt.get("cases").forEach(casePath => { if (!casePath.node.test) { return; } keys.push(getCaseKey(casePath)); }); return keys; } function sameSequenceKeySet(order, caseKeys) { if (!order || order.length !== caseKeys.length) { return false; } const counts = new Map(); caseKeys.forEach((key) => counts.set(String(key), (counts.get(String(key)) || 0) + 1)); for (const item of order) { const key = String(item); const count = counts.get(key); if (!count) { return false; } if (count === 1) { counts.delete(key); } else { counts.set(key, count - 1); } } return counts.size === 0; } function getAncestorBodyStatements(loopPath) { const bodies = []; const seen = new Set(); let current = loopPath; while (current) { if (current.isProgram()) { const key = `program:${current.node.start || 0}`; if (!seen.has(key)) { bodies.push(current.get("body")); seen.add(key); } } else if (current.isBlockStatement() && Array.isArray(current.node.body)) { const key = `block:${current.node.start || 0}`; if (!seen.has(key)) { bodies.push(current.get("body")); seen.add(key); } } current = current.parentPath; } return bodies; } function collectObjectPipeStringCandidates(loopPath, objectName) { if (!objectName) { return []; } const objectNames = resolveObjectAliasNames(loopPath, objectName); const candidates = []; const loopStart = loopPath.node.start || 0; getAncestorBodyStatements(loopPath).forEach((statements) => { statements.forEach((stmtPath) => { if (!stmtPath.node || (stmtPath.node.start || 0) >= loopStart) { return; } if (stmtPath.isVariableDeclaration()) { stmtPath.get("declarations").forEach((declPath) => { if (!declPath.get("id").isIdentifier() || !objectNames.has(declPath.get("id").node.name) || !declPath.get("init").isObjectExpression()) { return; } declPath.get("init.properties").forEach((propPath) => { if (!propPath.isObjectProperty()) { return; } const keyPath = propPath.get("key"); const key = propPath.node.computed ? getStaticPropertyKey(keyPath.node) : getStaticPropertyKey(keyPath.node); const valuePath = propPath.get("value"); const pipeOrder = getPipeOrderFromValuePath(valuePath); if (pipeOrder) { candidates.push({ key, order: pipeOrder, start: stmtPath.node.start || 0 }); } }); }); return; } if (!stmtPath.isExpressionStatement()) { return; } const exprPath = stmtPath.get("expression"); if (!exprPath.isAssignmentExpression({ operator: "=" })) { return; } const leftPath = exprPath.get("left"); const rightPath = exprPath.get("right"); if (!leftPath.isMemberExpression() || !leftPath.get("object").isIdentifier() || !objectNames.has(leftPath.get("object").node.name)) { return; } const key = leftPath.node.computed ? getStaticPropertyKey(leftPath.node.property) : getStaticPropertyKey(leftPath.node.property); if (key === null) { return; } const pipeOrder = getPipeOrderFromValuePath(rightPath); if (pipeOrder) { candidates.push({ key, order: pipeOrder, start: stmtPath.node.start || 0 }); } }); }); candidates.sort((a, b) => b.start - a.start); return candidates; } function getOrderFromObjectStringTable(loopPath, switchStmt) { const discriminant = switchStmt.get("discriminant"); if (!discriminant.isMemberExpression() || !discriminant.get("object").isIdentifier()) { return null; } const candidates = collectObjectPipeStringCandidates(loopPath, discriminant.get("object").node.name); if (candidates.length === 0) { return null; } const caseKeys = getCaseKeys(switchStmt); const match = candidates.find((candidate) => sameSequenceKeySet(candidate.order, caseKeys)); return match ? match.order : null; } function flatten(ast) { let changed = false; traverse(ast, { ForStatement(path) { const body = path.get("body"); if (!body.isBlockStatement() || body.get("body").length === 0) { return; } const firstStmt = body.get("body.0"); if (firstStmt.isSwitchStatement() && path.get("init").isVariableDeclaration() && path.node.update === null) { let order = null; const leftovers = []; path.get("init.declarations").forEach(decl => { if (decl.node.init === null) { leftovers.push(t.variableDeclaration("var", [clone(decl.node)])); return; } const candidate = getOrderFromInit(decl.get("init")) || getOrderFromSplitCallWithObjectTable(decl.get("init"), path) || getOrderFromResolvedValue(resolveStatic(decl.get("init"))); if (candidate) { order = candidate; } }); if (order) { const caseMap = new Map(); firstStmt.get("cases").forEach(casePath => { if (!casePath.node.test) { return; } caseMap.set(getCaseKey(casePath), getCaseStatements(casePath)); }); const blocks = [...leftovers]; order.forEach(key => { const stmts = caseMap.get(key); if (stmts) { blocks.push(...stmts); } }); if (blocks.length > 0) { path.replaceWithMultiple(blocks); changed = true; return; } } } if (replaceLoopSwitchWithIf(path)) { changed = true; } }, WhileStatement(path) { if (path.get("test").isUnaryExpression() && path.get("body").isBlockStatement()) { const switchStmt = path.get("body.body.0"); if (switchStmt.isSwitchStatement()) { const nearby = findNearbyOrderDeclaration(path); const order = (nearby && nearby.order) || getOrderFromDiscriminant(switchStmt.get("discriminant")) || getOrderFromObjectStringTable(path, switchStmt); const blocks = buildBlocksFromSwitchOrder(switchStmt, order); if (blocks) { path.replaceWithMultiple(blocks); if (nearby && nearby.declarationPath && nearby.declarationPath.node) { nearby.declarationPath.remove(); } changed = true; return; } } } if (replaceLoopSwitchWithIf(path)) { changed = true; } } }); return { ast, changed }; } const { inputPath, outputPath } = parseArgs(); let ast = parseFile(inputPath); let changed = false; do { ({ ast, changed } = flatten(ast)); if (changed) { ast = reparse(ast); } } while (changed); saveAst(ast, outputPath);