Back to Hub
COMPILER THEORY •
By Yash Panchal · Last updated:

Flawless Auto-Formatting via AST Manipulation.

Most basic online code editors use simple Regular Expressions to format code. They look for curly braces {} and add indentation. This approach falls apart instantly when faced with complex ES6 template literals, nested JSX, or ternary operators. Regex cannot understand context.

NitroIDE implements true, context-aware code formatting by leveraging the Babel parser to generate an Abstract Syntax Tree (AST). We don't format strings; we manipulate the underlying logical structure of the code.

The AST Lifecycle

When you press Cmd + Shift + F, the editor sends the raw string to a Web Worker. Babel parses the string into an enormous JSON tree representing every variable, function, and operator. We then traverse this tree, applying formatting rules to the nodes themselves, before generating a new, perfectly formatted string.

// Parsing, transforming, and generating code via Babel AST
import * as babel from '@babel/core';

const formatCode = (rawCode) => {
  // 1. Parse string to AST
  const ast = babel.parse(rawCode, { sourceType: 'module' });
  
  // 2. Traverse and normalize nodes (e.g., standardizing quotes)
  babel.traverse(ast, {
    StringLiteral(path) {
      path.node.extra.raw = `'${path.node.value}'`; // Force single quotes
    }
  });
  
  // 3. Generate perfectly formatted string from AST
  const { code } = babel.generate(ast, { retainLines: false });
  return code;
};

Destructive Transformations: Because we operate on the AST, we can safely perform destructive actions like removing unused imports or rewriting arrow functions, with mathematical certainty that we won't break the actual execution logic.

Worker Synchronization

Because AST generation is CPU-intensive, running it on the main thread would freeze the editor. By executing the Babel traversal inside a SharedWorker, NitroIDE can format a 10,000-line React component in the background while you continue typing, applying the update seamlessly once complete.

Why Regex Formatters Break

To appreciate the AST approach, it helps to see exactly where regex gives up. Consider a template literal like `total: ${items.length}` — it contains braces that are not blocks and a dollar-brace sequence that is not a variable declaration. A regex-based indenter sees the { and happily indents the next line. Or take JSX: <div>{items.map(i => <span key={i.id}>{i.name}</span>)}</div> — angle brackets that are not HTML tags, comparison operators that look like tag closers, and ternaries stretching across lines.

Every one of these cases requires understanding context: is this brace inside a string? Is this < starting a JSX element or comparing two numbers? Regular expressions are, by definition, context-free — they match patterns in a flat character stream. You can pile on heuristics until simple cases work, but the next nested ternary or tagged template will break them. The failure mode is also silent: the formatter confidently produces wrong code, and you discover it when something renders incorrectly.

An AST never has this problem because it never sees characters — it sees structure. The parser has already decided which braces are blocks, which are object literals, and which are template placeholders before formatting begins. There is simply no ambiguity left to get wrong.

What the Tree Actually Looks Like

Parsing const answer = 42; produces a nested structure where every syntactic element becomes a typed node. Simplified, it looks like this:

// babel.parse("const answer = 42;") — simplified
{
  "type": "File",
  "program": {
    "type": "Program",
    "body": [{
      "type": "VariableDeclaration",
      "kind": "const",
      "declarations": [{
        "type": "VariableDeclarator",
        "id": { "type": "Identifier", "name": "answer" },
        "init": { "type": "NumericLiteral", "value": 42 }
      }]
    }]
  }
}

Every node carries its type plus position information (start, end, and line/column locations), which is how the editor maps formatted output back to your cursor position and how error messages point at the exact offending token. The formatter's job reduces to a clean recursive walk: visit each node, decide line breaks and indentation from the node type and its depth in the tree, then print. A BlockStatement always gets braces on their own lines; a JSXElement indents its children; a ConditionalExpression breaks after the ? and : when it exceeds the print width.

This is also why AST formatting is stable: formatting already-formatted code produces byte-identical output, because the tree is identical. Regex formatters drift — run them twice and watch the indentation creep. The tree has no memory of the original whitespace, so there is nothing to drift.

Beyond Formatting: Safe Structural Edits

Once you hold the tree, formatting is the shallow end. The same traversal API enables edits that would be genuinely dangerous as string operations. Removing unused imports is the classic example: walk the tree collecting every referenced identifier, then drop any import whose bindings never appear. The subtlety — which a regex could never handle — is excluding the import statements themselves from the "used" set, since an import's specifiers are technically identifiers too:

// Codemod: drop imports that are never referenced
traverse(ast, {
  Program(path) {
    const used = new Set();
    path.traverse({
      Identifier(idPath) {
        if (idPath.parentPath.isImportSpecifier()) return; // don't count the import itself
        used.add(idPath.node.name);
      }
    });
    path.get('body')
      .filter(p => p.isImportDeclaration())
      .filter(p => p.node.specifiers.every(s => !used.has(s.local.name)))
      .forEach(p => p.remove());
  }
});

Renaming works the same way through scope analysis: scope.rename() updates every binding reference while respecting shadowing, so renaming a variable never accidentally captures an unrelated identifier with the same name in a nested scope. Try doing that safely with find-and-replace. These are the same tree operations that power the browser-based JSX transpilation in our React workspace — one engine, two very different applications.

Keeping the UI Smooth: Formatting Off the Main Thread

Parsing is linear in file size, but the constant factors are real: building a tree for ten thousand lines of React involves hundreds of thousands of node allocations. Do that on the main thread and the editor stutters — keystrokes queue up, the preview freezes, and the whole experience feels broken. That is why the traversal runs in a worker, and why the design details matter.

The worker receives the raw text, parses and transforms it in isolation, and posts back the formatted result along with any diagnostics. The main thread then applies the result as a single atomic edit rather than replacing the document wholesale, which preserves your cursor position, selection, and undo stack. You keep typing while the worker thinks; the formatted text lands when it's ready, exactly where you left off.

This architecture also enables format-on-paste and format-on-save as background operations — small enough to feel instant, safe enough to run unattended, because the tree guarantees the output is semantically identical to the input. The binding is one keystroke: Cmd/Ctrl + Shift + F, listed alongside every other editor action on the keyboard shortcut cheatsheet and in our keybinding guide if you prefer modal editing.

Free syntax checking: AST formatting fails loudly on invalid code — and that's a feature. If the parser can't build a tree, the formatter reports the exact line and column of the syntax error instead of reformatting. Pasting suspect code and hitting format is a legitimate quick-and-dirty lint pass.

Format Code Instantly.

Paste ugly, unformatted React code into our sandbox and watch the AST engine fix it.

Launch React Sandbox