Kelick Tech Hub

Build, learn & keep up — tech, Linux & sports news included.

C++ Code Auto-formatter Project

Advanced•1–2 weeks•tokenising, parsing, text generation

Feed it messy C++ and get consistently indented, styled code back.

Share on X

What you will build

A clang-format in miniature: tokenise source code, then re-emit it with your style rules — brace placement, indentation, spacing around operators. Formatters are parsers with opinions; building one teaches lexing, token streams, and generation in a tight loop.

What you will learn

Feature checklist

Starter code

#include <string>
#include <vector>
#include <sstream>

struct Style { int indentWidth = 4; bool bracesOnNewLine = false; };

// Tokens keep their kind so strings/comments pass through untouched.
enum class Kind { Word, Number, Str, Chr, Comment, Sym };
struct Tok { Kind kind; std::string text; };

std::string format(const std::vector<Tok>& toks, const Style& st) {
    std::ostringstream out;
    int depth = 0;
    auto newline = [&]() {
        out << "\n" << std::string(depth * st.indentWidth, ' ');
    };
    for (size_t i = 0; i < toks.size(); ++i) {
        const Tok& t = toks[i];
        if (t.text == "{") {
            if (st.bracesOnNewLine) newline();
            else out << " ";
            out << "{"; ++depth; newline();
        } else if (t.text == "}") {
            --depth; newline(); out << "}";
        } else if (t.text == ";") {
            out << ";"; newline();
        } else {
            if (i && toks[i - 1].text != "{" && toks[i - 1].text != ";")
                out << " ";
            out << t.text;
        }
    }
    return out.str();
}

Full walkthrough

Step 1: Decide scope honestly: a formatter for a subset

This tool reformats C-like code (braces, semicolons, operators) with a configurable Style { int indentWidth; bool braceOnNewLine; }. It is not clang-format and must never pretend to be: it handles the constructs in its test corpus correctly and says so. Scoping truthfully is what makes the idempotency guarantee below achievable.

Step 2: Tokenise before you format: struct Tok

struct Tok { Kind kind; std::string text; } with kinds like Ident, Number, String, Char, Op, Brace, Semicolon. The lexer from the mini compiler returns — same skill, new purpose — with one vital upgrade: string literals, char literals, and COMMENTS are single tokens whose text is sacred. A formatter that 'fixes' spacing inside "a + b" or a // comment is destroying data; the tokeniser is where that protection lives.

// '// not a comment' inside a string token stays untouched
// because the lexer consumed the whole literal first

Step 3: format(): re-emit tokens under rules

format(source, style) walks the tokens carrying indentDepth: '{' increases depth (after its line), '}' decreases BEFORE its line, ';' ends a statement line, operators get single spaces around them, commas get a space after, function-call '(' gets none before. Output is rebuilt line by line from tokens — never by patching the original text — so spacing in the input is irrelevant, which is the entire idea of a formatter.

Step 4: Handle the constructs that break naive versions

for (i = 0; i < n; ++i): the semicolons inside a for-header must NOT end the line — track paren depth and treat ';' as a separator while depth > 0. if/else chains and '} else {' joining under same-line brace style; unary minus vs binary minus by previous token kind; 'std::' and pointer '*' spacing by context. Each of these is a small state rule in the walker — collect them as you discover them, with a test per rule.

Step 5: Guarantee idempotency — and test it as a property

format(format(x)) == format(x) for every test file, asserted programmatically. Idempotency is the formatter's honesty certificate: if the second pass changes anything, the first pass produced output your own rules consider wrong. Property tests (this, plus 'formatting never changes the token sequence except whitespace') catch classes of bugs that example tests never will.

Step 6: Process whole folders via <filesystem>

Recursively find .cpp/.h files, format each, and either overwrite in place (with a --check mode that only REPORTS which files would change, and a backup or VCS warning). A dry-run/check mode is the difference between a tool developers trust on their codebase and a script they run once on a copy.

Step 7: Test against a messy corpus

Keep a folder of deliberately ugly files plus their expected outputs: crushed spacing, wild indentation, strings containing fake code, comments containing braces ('// }' must not dedent your file — comment tokens again). Every formatter bug you ever fix gets a corpus file first. The corpus IS the specification.

Common mistakes

Compile and run

g++ -std=c++17 -o formatter formatter.cpp

Sample run

$ ./formatter
Style: indent 4 · braces on the same line · from .style
Formatting messy.cpp...
--- before ---
int main(){int x=1+2;if(x>2){std::cout<<x;}return 0;}
--- after ---
int main() {
    int x = 1 + 2;
    if (x > 2) {
        std::cout << x;
    }
    return 0;
}
Idempotency check: format(after) == after — PASS
Token check: string and comment contents unchanged — PASS

$ ./formatter --check src/
src/engine.cpp — would reformat
src/main.cpp — already formatted
2 files checked, 1 would change, 0 errors

Try it in your browser

No browser demo is included for this project. Build the C++ version below — that is the real program.

Related Advanced projects

← All 60 projects · Classic site (Learn & Sandbox)

Download Fake GPS