DEV Community

Finley Zhou
Finley Zhou

Posted on

Case Study: A Free Model Wrote a C++ TODO Expiry Checker. Compilation Passed; the Fixture Didn't.

TODO comments rot. They accumulate in the codebase, unsigned, undated, and unowned. Nobody greps for them until the debt is already visible. This case study turns that graveyard into a CI gate: a small C++17 tool that scans a repo, extracts ISO dates from TODO and FIXME comments, and fails the build when a deadline has passed.

I used MonkeyCode's free model access to draft the scanner, and its free server option to run the compile-test loop. The loop took three rounds. Two drafts failed in instructive ways. The fixture, not the compiler, found the truth.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

The Problem: TODO Comments Don't Age

A TODO without a date is a wish. A TODO with a date is a promise. Most repos only have wishes.

The goal was small and concrete:

  1. Scan a directory recursively for .cpp, .hpp, .h, and .cc files.
  2. Find comments containing TODO or FIXME.
  3. Parse an ISO date (YYYY-MM-DD) from the comment.
  4. If the date is older than a 90-day grace period, report it as expired.
  5. Exit with code 1 if any expired TODO exists, so CI can fail.

The tool also had to ignore three traps: TODO-looking strings inside string literals, TODO tokens inside multi-line block comments, and invalid dates like 2026-02-31.

The Fixture Came First

Before any code, I built the test fixture. It is the artifact that makes this case study reproducible:

  • 23 source files
  • 47 TODO/FIXME tokens:
    • 12 dated and expired (before 2026-05-26, given a 90-day grace from 2026-08-24)
    • 9 dated and still active
    • 8 undated
    • 5 inside string literals (must be ignored)
    • 4 inside multi-line block comments (must be found)
    • 1 malformed date, 2026-02-31 (must be reported, not accepted)
    • 8 control comments with NOTE or HACK (must not match)

Expected result: exit code 1, exactly 12 expired findings, zero false positives, one malformed-date error.

Round 1: It Compiled. It Was Wrong.

The first draft compiled on the first try. That was the only thing it did correctly.

The model used std::regex to find dates and searched the raw line for TODO. The regex matched, but the approach had two holes:

  • String literals like const char* s = "TODO: 2020-01-01"; were reported as expired TODOs. Five false positives.
  • 2026-02-31 was accepted as a valid date. The model never validated month lengths.

Compilation succeeded. The fixture failed. That is the core lesson of Round 1: a green build is a weak signal.

Round 2: The C++20 Trap

The second draft fixed the string-literal problem by adding a state machine. Then it introduced a new problem: it used std::chrono::year_month_day and std::chrono::days, which are C++20 types. The project toolchain is C++17.

The draft did not compile. The error was immediate, but the failure mode is common: free models often reach for the newest standard library features without checking the target standard.

The fix was to compute dates with Howard Hinnant's days_from_civil algorithm, which is C++17-safe:

int days_from_civil(int y, unsigned m, unsigned d) {
    y -= m <= 2;
    const int era = (y >= 0 ? y : y - 399) / 400;
    const unsigned yoe = static_cast<unsigned>(y - era * 400);
    const unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;
    const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
    return era * 146097 + static_cast<int>(doe) - 719468;
}
Enter fullscreen mode Exit fullscreen mode

Today's date is converted the same way, and the age is a simple integer subtraction:

auto now = std::chrono::system_clock::now();
std::time_t t = std::chrono::system_clock::to_time_t(now);
std::tm tm{};
localtime_r(&t, &tm);  // or localtime_s on Windows
int today = days_from_civil(tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday);
int age_days = today - days_from_civil(y, mo, d);
Enter fullscreen mode Exit fullscreen mode

No C++20 types. No timezone library. Just integers.

Round 3: The State Machine

The third draft combined both fixes: a line-oriented state machine that tracks string literals and block comments across lines, plus validated date parsing.

The core scanner walks each line character by character:

struct ScanState {
    bool in_block = false;
    bool in_string = false;
};

// Simplified core: returns true if the TODO/FIXME token at line[i]
// is inside a comment and not inside a string literal.
bool token_is_in_comment(const std::string& line, size_t i, ScanState& st) {
    for (size_t j = 0; j < i; ++j) {
        char c = line[j];
        if (st.in_string) {
            if (c == '\\') { ++j; continue; }
            if (c == '"') st.in_string = false;
            continue;
        }
        if (st.in_block) {
            if (c == '*' && j + 1 < line.size() && line[j + 1] == '/') {
                st.in_block = false;
                ++j;
            }
            continue;
        }
        if (c == '"') { st.in_string = true; continue; }
        if (c == '/' && j + 1 < line.size()) {
            if (line[j + 1] == '/') return true;  // line comment
            if (line[j + 1] == '*') { st.in_block = true; ++j; }
        }
    }
    return st.in_block;
}
Enter fullscreen mode Exit fullscreen mode

The date parser validates ranges before converting:

if (mo < 1 || mo > 12 || d < 1 || d > 31) return std::nullopt;
Enter fullscreen mode Exit fullscreen mode

That check is deliberately simple. It rejects 2026-02-31. A full calendar validation is possible, but for a CI gate, rejecting impossible dates is enough.

Results

The three rounds produced a clear table:

Draft Compiled (C++17) Expired found False positives Verdict
1 yes 12/12 6 (5 strings + 1 invalid date) fail
2 no (C++20 API) fail
3 yes 12/12 0 pass

Round 3 also reported the malformed date as an error and ignored all 8 control comments. The exit code was 1, as designed.

The full loop ran on MonkeyCode's free server option: compile, run fixture, compare output. No local resources were consumed, and the iteration cost was negligible. That is the real value of the setup — not the code quality of any single draft, but the ability to fail cheaply and often.

Lessons

  1. Compilation is not correctness. Draft 1 compiled and was wrong in two ways. The fixture caught both in seconds.
  2. Define the traps before the code. The fixture forced the model to handle strings, block comments, and invalid dates. Without it, the false positives would have shipped.
  3. The target standard matters. Draft 2 failed because it assumed C++20. The standard should be part of the prompt and part of the build.
  4. A state machine beats a regex for comment scanning. Regex is fine for finding a date. It is not fine for knowing whether a token is inside a comment.

Limitations

  • The tool only understands YYYY-MM-DD. Formats like 2026/08/24 or 24 Aug 2026 are ignored.
  • Undated TODOs are warnings, not errors, unless --require-date is passed.
  • The string-literal state machine does not handle raw string literals (R"(...)"). They are rare in this codebase, but they would produce false positives.
  • The grace period is a single global constant. Per-owner or per-module grace periods are not supported.

Who Should Not Use This

Skip this approach if any of these apply:

  • Your CI is unreliable. A flaky gate is worse than no gate.
  • Your team treats TODO comments as permanent documentation. Forcing expiry will cause noise, not cleanup.
  • Your repo is small enough that grep -rn "TODO" gives you the full picture in one screen.
  • You cannot tolerate false positives. Every false positive trains the team to ignore the gate.

For everyone else, the pattern is worth copying: define the fixture first, let a free model draft the tool, and let the fixture — not the compiler — judge the result.

If you want to try it, the fixture layout in this post is a good starting point. Add your own traps. The model will find new ways to fail.

Top comments (0)