These recipes are practical patterns you can adapt in your own analyzers, refactorers, and deobfuscation pipelines.
- Analyze A File
- Transform A File And Write The Result
- Recipe: Proxy Variable Detection
- Recipe: Replace Proxy References
- Recipe: Find Computed Members
- Recipe: Fold Constant Addition
- Recipe: Match/Transform Separation
- Recipe: Identify Wrapper IIFEs
- Recipe: Inspect Scopes
- Recipe: Always Edit With
Arborist
import fs from 'node:fs';
import {generateFlatAST} from 'flast';
const code = fs.readFileSync(process.argv[2], 'utf8');
const ast = generateFlatAST(code);
console.log({
nodes: ast.length,
identifiers: ast[0].typeMap.Identifier.length,
calls: ast[0].typeMap.CallExpression.length,
literals: ast[0].typeMap.Literal.length,
});import fs from 'node:fs';
import {Arborist} from 'flast';
const inputPath = process.argv[2];
const outputPath = process.argv[3] ?? 'output.js';
const arb = new Arborist(fs.readFileSync(inputPath, 'utf8'));
for (const node of arb.ast[0].typeMap.Literal) {
if (node.value === 'old-value') {
arb.replaceNode(node, {
type: 'Literal',
value: 'new-value',
raw: "'new-value'",
});
}
}
arb.applyChanges();
fs.writeFileSync(outputPath, arb.script, 'utf8');Find bindings such as const alias = realName;.
import {generateFlatAST} from 'flast';
function findProxyVariables(code) {
const ast = generateFlatAST(code);
const matches = [];
for (const n of ast[0].typeMap.VariableDeclarator) {
if (
n.id?.type === 'Identifier' &&
n.init?.type === 'Identifier' &&
n.id.name !== n.init.name
) {
matches.push(n);
}
}
return matches;
}import {applyIteratively} from 'flast';
function replaceProxyReferences(arb) {
for (const node of arb.ast[0].typeMap.VariableDeclarator) {
if (node.id?.type === 'Identifier' && node.init?.type === 'Identifier') {
for (const ref of node.references || []) {
arb.replaceNode(ref, {
type: 'Identifier',
name: node.init.name,
});
}
}
}
return arb;
}
const result = applyIteratively('var a = b; console.log(a);', [replaceProxyReferences]);
console.log(result);Useful for patterns such as console["log"] or obj[key].
import {generateFlatAST} from 'flast';
function findComputedMembers(code) {
const ast = generateFlatAST(code);
return ast[0].typeMap.MemberExpression.filter((n) => n.computed);
}import {applyIteratively} from 'flast';
function foldConstantAddition(arb) {
for (const node of arb.ast[0].typeMap.BinaryExpression) {
if (
node.operator === '+' &&
node.left.type === 'Literal' &&
node.right.type === 'Literal' &&
typeof node.left.value === 'number' &&
typeof node.right.value === 'number'
) {
const value = node.left.value + node.right.value;
arb.replaceNode(node, {type: 'Literal', value, raw: String(value)});
}
}
return arb;
}This pattern scales well once your transforms become more complex.
function matchCandidates(arb) {
return arb.ast[0].typeMap.CallExpression.filter((n) =>
n.callee?.type === 'Identifier' && n.callee.name === 'debug',
);
}
function transformMatches(arb, matches) {
for (const n of matches) {
arb.replaceNode(n.callee, {
type: 'Identifier',
name: 'console',
});
}
return arb;
}
function runTransform(arb) {
const matches = matchCandidates(arb);
transformMatches(arb, matches);
arb.applyChanges();
return arb.script;
}import {generateFlatAST} from 'flast';
function findWrapperIifes(code) {
const ast = generateFlatAST(code);
return ast[0].typeMap.CallExpression.filter((node) =>
node.callee?.type === 'FunctionExpression' ||
node.callee?.type === 'ArrowFunctionExpression',
);
}import {generateFlatAST} from 'flast';
const ast = generateFlatAST(`
function outer() {
const x = 1;
function inner() {
return x;
}
}
`);
for (const node of ast[0].typeMap.Identifier) {
console.log(node.name, {
scopeType: node.scope?.type,
scopeId: node.scope?.scopeId,
lineage: node.lineage,
ancestry: node.ancestry,
});
}Use Arborist for code changes so replacements and deletions stay validated and comment-safe.
Reach for Arborist when:
- Deleting nodes
- Replacing nodes
- Editing array-backed statement lists
- Preserving validity and comments matters
Typical workflow:
- Create
const arb = new Arborist(script) - Queue edits with
replaceNode()anddeleteNode() - Commit them with
applyChanges() - Read the updated source from
arb.script