The Quiet Problem with AI-Generated Code: It Never Gets Tired
There is a pattern I kept noticing after integrating AI coding assistants into my workflow. The code worked. Tests passed. And yet, three months later, I was sitting in front of a 600-line Rust file with a 150-line function in the middle of it, wondering how it got there.
No one wrote that function maliciously. No one sat down and thought “I’ll make this as hard to read as possible.” The AI wrote it. I accepted it. And the review never caught it because the code was correct.
That is the problem I want to talk about.
What teams are noticing
If you use AI coding assistants regularly, you have probably seen this. You ask for an implementation and get back something that works, handles the edge cases you mentioned, maybe even adds a few you did not think of. But it is all in one function. Or one file. The abstractions that would normally emerge from a human developer sitting with the problem for an hour — the helper function they would extract because they got tired of reading the same block twice, the module boundary they would draw because the file was getting unwieldy — those do not appear.
I have spoken with other developers about this and the reaction is usually a variation of “yes, I’ve noticed that too” followed by a shrug. It feels like a minor complaint against something that is otherwise genuinely useful, so it does not get addressed. But it compounds. A codebase where every AI-assisted PR adds a few oversized functions and a bloated file or two will, over the course of six months, become materially harder to work in.
Why AI tools produce this
This is not a criticism of any specific tool. It is a consequence of how these systems generate code.
When a language model produces a function, it is optimizing for completing the task described in the prompt. The pressure to decompose — to split into smaller units, to extract, to separate concerns — comes from the cognitive load a human experiences when writing long code. You get tired. You lose track of what you are reading. You create a helper function because your working memory has limits.
The model does not have that experience. A 200-line function is not harder to write than a 20-line one. There is no discomfort signal that triggers the refactor. The model can also not easily look at your project’s module structure and decide where a new file should live, so it defaults to putting everything in the file it is already working with. Splitting a file requires understanding the broader architecture — which imports what, where responsibility boundaries are — and that context is not always available in the prompt.
The result is code that is correct but structurally different from what an experienced developer would write after sitting with the problem. Correctness is necessary but not sufficient for a codebase that needs to stay maintainable.
What the existing tools miss
The obvious response is “use a linter.” Most linters, though, do not check function length. They check style, undefined variables, type errors, security patterns. Some have complexity metrics like cyclomatic complexity, which is a related but different concern — a function can be long and simple, or short and deeply complex.
The other common approach is wc -l. Count the lines in a file, flag anything over a threshold. This is imprecise in a way that matters. A 300-line Python file might be mostly docstrings and blank lines with five small functions inside. A 150-line file might contain one function that does the work of four. File line counts measure the wrong unit. Functions are the unit of comprehension.
What I wanted was something that understood function boundaries. Not a heuristic — not counting indentation levels or looking for the word def — but something that actually parsed the code and found the real boundaries, including nested functions, closures, and arrow functions.
Building codesize
I wrote codesize to fill that gap.
It uses tree-sitter to build an AST for each source file and walks it to find function boundaries. This means it counts only the lines that actually belong to each function — from signature to closing brace — without being confused by blank lines, comments, or strings that happen to contain braces. Nested functions in Rust and Python are counted independently. Arrow functions in TypeScript and JavaScript are counted as functions. Constructors in Java are counted.
It supports ten languages out of the box: Rust, TypeScript, JavaScript, Python, Go, Java, C, C++, Swift, and Lua. For any other language you can add an extension mapping in config and still get file-level enforcement, even without function-level analysis.
The output is a CSV:
language, exception, function, codefile, lines, limit
Rust, function, build_report, src/scanners.rs, 95, 80
Python, file, , src/legacy/monolith.py, 450, 300
Six columns: language, whether it is a file or function violation, the function name if applicable, the file path, the actual line count, and the configured limit. Rows are sorted by language, then by line count descending, so the worst offenders are at the top.
The CSV format is intentional. You can import it into a spreadsheet, pipe it into a task tracker, or use it as structured output in a CI job. The goal is not just to flag violations but to create a list of work items you can actually act on.
Plugging it into the AI workflow
This is where it connects back to the original problem. If your team is using AI assistants to write substantial amounts of code, you want codesize running in CI so that violations are caught before they merge — not discovered three months later when someone is trying to understand why a file is 600 lines.
The GitHub Action is two lines:
- uses: actions/checkout@v4
- uses: ChrisGVE/codesize-action@v1.0.0
With --fail, codesize exits with status 1 if any violations are found, which is what CI needs to block a merge.
The tolerance flag is useful when you are introducing this into an existing codebase that already has violations:
# Allow 20% headroom while working through the backlog
codesize --tolerance 20 --gitignore
Start at 30% or 40% so nothing currently in the codebase is blocked. Then tighten it over a series of sprints as you work through the backlog of existing violations. This gives you a ratchet: new code has to meet the current threshold, and the threshold tightens over time.
The per-language limits are configurable:
[limits.Rust]
file = 500
function = 80
[limits.Python]
function = 50
What this changes in practice
The framing I find useful is that codesize enforces the same structural constraint that a good code reviewer would apply, but automatically and consistently. A reviewer catching a 150-line function in a PR is doing cognitive work. A CI check that catches it requires no attention at all.
When AI assistants are writing or contributing to a meaningful fraction of your code, that automatic enforcement matters more, not less, because the usual human signals that prevent oversized code from being written in the first place are not present. The AI does not notice that the function is getting long. The CI check does.
For teams who have a code review culture that already catches this kind of thing, codesize is redundant. But in my experience, even conscientious teams miss function length in review — because the code works, the logic is right, and the length is not the thing that draws your eye. Having an automated gate that specifically looks for this, and surfaces it as a list of named violations, changes what gets fixed.
codesize is available at https://github.com/ChrisGVE/codesize.
brew install ChrisGVE/tap/codesize
# or
cargo install codesize