Skip to content
Engineering

How Ovvoc Uses AST Transforms to Fix Breaking Changes

Ovvoc team··8 min read

When a dependency update changes an API, someone has to update every call site in your codebase. Most tools leave that to you. Ovvoc does it automatically — not with regex, but with Abstract Syntax Tree (AST) transforms.

Why regex fails

String-based find-and-replace seems like the obvious approach. Rename a function? Just s/oldName/newName/g. But real code is full of ambiguity:

  • Variable names that match the function name
  • Strings that contain the function name
  • Comments referencing the function
  • Different import aliases
  • Destructured parameters with the same name

A regex replacement that turns app.delete() into something else will also match user.delete(), strings like "please delete", and comments explaining deletion logic. Every false positive is a bug you introduced.

What an AST is

An Abstract Syntax Tree is a structured representation of your source code. Instead of treating code as a flat string, a parser converts it into a tree where each node represents a syntactic construct: a function call, an import declaration, a variable assignment, a string literal.

When Ovvoc needs to rename app.delete(), it doesn't search for the string. It walks the tree, finds CallExpression nodes where the callee is a MemberExpression with the property delete on an object of type Express.Application, and rewrites only those nodes.

OXC: why the fastest parser matters

Ovvoc uses OXC (Oxidation Compiler), the fastest JavaScript/TypeScript parser available. Written in Rust, OXC parses code 5–10x faster than Babel and produces a full-fidelity AST with parent node access and semantic analysis.

Speed matters because Ovvoc processes entire repositories. A large monorepo might have hundreds of files that need scanning. With OXC, the full scan takes milliseconds, not seconds.

Transform example: Express 4 to 5 wildcards

Express 5 uses path-to-regexp v8, which changes wildcard syntax. The bare * wildcard must become {*path}:

Express 4 (before)
app.get('/files/*', serveFiles);
app.use('/api/*', apiRouter);
Express 5 (after)
+ app.get('/files/{*path}', serveFiles);
+ app.use('/api/{*path}', apiRouter);

The AST transform identifies CallExpression nodes where the callee is app.get, app.use, app.post, etc., checks whether the first argument is a string literal containing * in route position, and rewrites only the route parameter — leaving everything else untouched.

11 transform types

Ovvoc supports 11 categories of AST transforms, each with its own matching logic:

  1. Function renameoldName()newName()
  2. Method renameobj.old()obj.new()
  3. Import path changefrom 'old/path'from 'new/path'
  4. Named export rename{ old }{ new }
  5. Default to named importimport Ximport { X }
  6. Parameter reorder — swap argument positions in function calls
  7. Wrapper addition — wrap a value in a new function call
  8. Property access changeobj.oldobj.new
  9. Config key migration — rename keys in configuration objects
  10. Express wildcard*{*path}
  11. Express optional param:param?{/:param}

When AST isn't enough: the AI fallback

Some changes are too context-dependent for deterministic rules. A paradigm shift from class components to hooks, or a middleware API redesign, requires understanding the intent of the code — not just its structure.

For these cases, Ovvoc falls back to AI-assisted migration. The AI receives a narrowly scoped prompt with the specific file, the specific change needed, and examples of the correct transformation. Its output is validated against the AST to ensure correctness, and the result is always verified by the full build-and-test pipeline before becoming a PR.

The AI is never given free rein. It's a tool in the pipeline, not the pipeline itself.

What is an AST?

An Abstract Syntax Tree is the structured representation that every compiler and interpreter uses internally. When you write const x = foo(1, 2);, the parser doesn't see a string of characters — it sees a tree of nodes:

  • A VariableDeclaration node (the const statement)
  • A VariableDeclarator node (the x = ... binding)
  • A CallExpression node (the foo(1, 2) invocation)
  • An Identifier node for the callee (foo)
  • Two NumericLiteral nodes for the arguments (1 and 2)

Every construct in JavaScript and TypeScript has a corresponding node type:ImportDeclaration for imports, MemberExpression for property access like obj.prop, ArrowFunctionExpression for arrow functions, and so on. The tree preserves the structure of the code while discarding irrelevant details like whitespace and formatting.

The core cycle is: Parse (source code → AST) → Transform (modify specific nodes) → Codegen (AST → source code). This is the same process that Babel, TypeScript, and every modern bundler uses internally. Ovvoc leverages this cycle to make surgical, targeted code modifications.

Why OXC over Babel and SWC

The JavaScript ecosystem has multiple parser options, each with different tradeoffs. We evaluated all of them and chose OXC for several compelling reasons.

Babel is the most widely used JavaScript parser and transformer. It's written in JavaScript, has a massive plugin ecosystem, and supports every syntax extension imaginable. But it's slow. Parsing a large file can take tens of milliseconds. For a tool that processes entire repositories with hundreds of files, those milliseconds add up to seconds — or even minutes for monorepos.

SWC (Speedy Web Compiler) was a major improvement. Written in Rust, it offered 10–20x speedup over Babel for most operations. But OXC takes performance further: it's 3–5x faster than SWC and 10–100x faster than Babel depending on the workload. OXC achieves this through arena-based memory allocation, zero-copy parsing, and a carefully optimized AST representation.

Beyond raw speed, OXC offers lower memory usage (critical when running inside memory-constrained containers), full TypeScript support without requiring type-checking, parent node access for context-aware transforms, and semantic analysis capabilities for understanding variable scoping and references.

Complete transform walkthrough

Let's walk through exactly what happens when Ovvoc transforms a lodash import to lodash-es, step by step:

  1. Read the source file — Ovvoc reads the target file from the cloned repository workspace
  2. Parse into AST with OXC — the source text is parsed into a full AST. For a TypeScript file, the parser handles type annotations, generics, and JSX transparently
  3. Walk the tree to find target nodes — Ovvoc traverses every node in the tree, looking for ImportDeclaration nodes where the source property matches 'lodash'
  4. Modify the matching nodes — for each match, the source value is changed from 'lodash' to 'lodash-es'. No other nodes are touched
  5. Regenerate source code — the modified AST is converted back to source text, preserving the original formatting style as closely as possible
  6. Write the file — the new source text replaces the original file in the workspace

The key insight is step 3: the tree walk only matches ImportDeclaration nodes with the correct source string. A variable named lodash, a comment mentioning lodash, or a string literal containing "lodash" in a log message — none of these areImportDeclaration nodes, so none of them are affected.

Multiple transform examples

Here are three representative transforms that illustrate the range of what AST manipulation can do:

Import rename (lodash → lodash-es): Find allImportDeclaration nodes where source.value === 'lodash' and change to 'lodash-es'. This converts CommonJS-style lodash to the ES module variant, enabling tree-shaking and reducing bundle size. The transform handles both default imports (import _ from 'lodash') and named imports (import { map, filter } from 'lodash').

Method rename (app.del → app.delete): Express 4 supportedapp.del() as an alias for app.delete() because delete is a reserved word in older JavaScript. Express 5 removes the alias. The AST transform findsCallExpression nodes where the callee is a MemberExpression with property name del on an Express app object, and changes the property todelete.

Parameter reorder: Some library updates change the order of function parameters. For example, a callback-first API might switch to an options-first API. The AST transform identifies matching CallExpression nodes and swaps the argument positions according to the defined mapping. This is impossible to do reliably with regex because arguments can be arbitrary expressions spanning multiple lines.

Edge cases and limitations

AST transforms are powerful, but they have boundaries. Understanding these limitations is important for knowing when to expect perfect automation and when human review adds value.

Comment preservation: Most AST representations don't include comments as first-class nodes. While OXC attaches comments to their nearest AST node, transforms that significantly restructure code may cause comments to shift position or, in rare cases, detach from the code they were documenting. Ovvoc preserves comments in the vast majority of cases, but reviewers should verify comment placement in complex transforms.

Formatting changes: Regenerating source code from an AST may produce slightly different formatting than the original. Semicolons, trailing commas, quote styles, and indentation may shift. This is cosmetic, not functional. If your project uses Prettier or ESLint with auto-fix, running your formatter after the transform produces a clean diff with only meaningful changes.

Dynamic imports: AST analysis is static — it operates on the source code as written. Code that uses eval(), dynamicrequire() with computed paths, or constructs import specifiers at runtime cannot be statically analyzed. Ovvoc flags these patterns in its reports so you know which files may need manual review.

Template literals with embedded code: Tagged template literals and other metaprogramming patterns embed code within strings. These string contents are opaque to the AST — the parser sees them as string values, not as executable code. If a migration affects APIs used inside template literals, those call sites require manual attention.

Stay up to date

Automate your dependency updates. Start with one repo.