C++ Code Auto-formatter Project
Feed it messy C++ and get consistently indented, styled code back.
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
- Lexing code without being fooled by strings/comments
- Token-stream transformation
- Tracking indent depth from braces
- Idempotency: formatting twice changes nothing
Feature checklist
- Re-indents by brace depth (configurable width)
- Normalises spacing around operators and commas
- Brace style: same-line or new-line
- Never alters strings, chars, or comments
- Idempotent: format(format(x)) == format(x)
- Processes whole folders of .cpp/.h files
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 firstStep 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
- Formatting with regexes on raw text, so braces inside strings and comments warp the indentation of the whole file.
- Splitting lines at every ';', exploding for-loop headers into three lines.
- Overwriting files with no --check/dry-run mode, making the tool unusable on any codebase its author values.
Compile and run
g++ -std=c++17 -o formatter formatter.cppSample 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 errorsTry it in your browser
No browser demo is included for this project. Build the C++ version below — that is the real program.