Cyclomatic Caps
Hard limits beat soft conventions.
A function that drifts past the linter's caps is doing too much. Extraction is the point of the rule, not its consequence. The cap is the lever; the linter is the surface.
Opinion
I have spent enough years watching engineers argue with style-guide warnings to know how the conversation ends. Soft conventions get ignored; hard limits get respected. The repo I work in caps cyclomatic complexity at 3 and function length at 15 lines.1complexity: ['warn', 3], max-lines-per-function: ['warn', { max: 15, skipBlankLines: true, skipComments: true }], max-depth: ['warn', 3], max-statements: ['warn', 10]. The cap is the lever; the linter is the surface. That is two orders of magnitude tighter than McConnell's 200-line ceiling and it is deliberate. Below the cap, anything goes; above it, the build fails and the function gets split. There is no “just this once” column in the lint config.
The receipts are old and they are unanimous. McCabe published the cyclomatic-complexity metric in 1976 and proposed v(G) ≤ 10 as a reasonable upper limit.2“A Complexity Measure”, IEEE Transactions on Software Engineering SE-2(4) (December 1976), pp.308–320. The foundational paper. Defines v(G) as the cyclomatic number of the control-flow graph; recommends v(G) ≤ 10 as “a reasonable, but not magical, upper limit”. NASA's Power-of-Ten rules give every safety-critical function the one-printed-page constraint and ban recursion outright.3“The Power of Ten — Rules for Developing Safety-Critical Code” (2006). Ten rules for code that flies on Mars; the function-length cap (Rule 4) and the restricted-control-flow rule (Rule 1) are the load-bearing two for this tenet. Linus put it in three lines in the kernel coding style: if you need more than 3 levels of indentation, you're screwed anyway, and should fix your program.4Linux kernel coding style §1 Indentation, §6 Functions: “If you need more than three levels of indentation, you're screwed anyway, and should fix your program.” The number changes per project; the discipline does not. Pick a number, write it into the linter, stop arguing.
The pairing matters. Cyclomatic Caps is the linter that forces decomposition; F1 Single Responsibility is the principle that decides where the cut goes; S4 Guard Clauses is the shape the surviving branches take. The cap is a hard edge that triggers the conversation; the other tenets carry the result. Without the cap, the conversation is a nag in code review and never wins.
Copy a note and link
Grab this short comment and drop it into a PR comment or an LLM chat to prompt the right change.
Functions that drift past the linter's caps are doing too much. Extraction is the point of the rule, not its consequence. McCabe complexity, function length and indentation depth all live in the linter; if the function trips the cap, refactor it before you raise the cap. /tenet/cyclomatic-caps/S2
AI eyes only
Rule: hard limits beat soft conventions. Function length and cyclomatic complexity are capped.
Reject: function bodies above the cap. Reject: requesting a complexity exemption. Reject: extracting one helper while leaving the parent over.
Generate: cut at early returns, guard clauses, and verb boundaries until each function is under the cap (default: 15 lines, complexity 3). Each helper has a verb-led name.
Diagnostic: run the lint after every edit. If the cap rule fires, fix the function; never silence the rule.
Why?
- Replaces the “is this too long?” argument in code review with a binary build outcome. Reviewers stop adjudicating taste; the linter does it.
- Operationalises F1 Single Responsibility Principle. A function that exceeds the cap is doing too much by definition; the cap forces the extraction conversation that SRP wants you to have anyway.
- Coding agents inherit the cap as a free spec. The model stops generating 80-line functions because it knows the lint will reject them; output sharpness improves inside a week of wiring the cap into pre-commit.
- Cuts review time. Short functions are easier to read; reviewers can hold the entire function in working memory; the diff is smaller and the failure modes are obvious.
- Reduces merge-conflict surface. Two engineers editing 15-line functions in the same module rarely collide; two engineers editing a 200-line function almost always do.
- Pairs cleanly with S4 Guard Clauses. Flattening early-returns is the cheapest way to drop cyclomatic complexity; the cap rewards the shape S4 prescribes.
- Improves test coverage almost for free. Each extracted function gets its own unit test; the coverage report rises without anyone writing more tests, because the tests they were going to write get to target a smaller surface.
Origins
Tom McCabe published “A Complexity Measure” in IEEE TSE in December 1976 and defined cyclomatic complexity v(G) as the number of linearly independent paths through a function's control-flow graph.2“A Complexity Measure”, IEEE Transactions on Software Engineering SE-2(4) (December 1976), pp.308–320. The foundational paper. Defines v(G) as the cyclomatic number of the control-flow graph; recommends v(G) ≤ 10 as “a reasonable, but not magical, upper limit”. McCabe's own recommendation was v(G) ≤ 10 — “a reasonable, but not magical, upper limit”. NIST later codified the threshold; most static-analysis tools default to it; the Prickles repo sets it at 3, which is McCabe's metric tightened by a factor of three.
Gerard Holzmann at NASA/JPL published “The Power of 10 — Rules for Developing Safety-Critical Code” in 2006, codifying the rules JPL used for Mars Exploration Rover flight software.3“The Power of Ten — Rules for Developing Safety-Critical Code” (2006). Ten rules for code that flies on Mars; the function-length cap (Rule 4) and the restricted-control-flow rule (Rule 1) are the load-bearing two for this tenet. Rule 1: restrict control flow to simple constructs. Rule 4: limit function bodies to one printed page. Rule 7: check the return value of every non-void function. The rules are written for code that has to land on Mars, but they have travelled into industrial style guides because the discipline they encode is universal.
Linus Torvalds put the same idea in three lines in the Linux kernel coding style: “Functions should do one thing”; “If you have a function that exceeds about three screenfuls, get out the indentation tool and rewrite it”; “If you need more than three levels of indentation, you're screwed anyway, and should fix your program.”4Linux kernel coding style §1 Indentation, §6 Functions: “If you need more than three levels of indentation, you're screwed anyway, and should fix your program.” The kernel ships with the rule baked into the contributor guidelines; the submission gets bounced before it reaches review.
Steve McConnell's Code Complete gave the much-cited finding that 65–200 lines is the empirically-defended sweet spot for routine length.6Code Complete, 2nd ed. (Microsoft Press, 2004). Ch. 7 “High-Quality Routines” summarises the empirical literature: 65–200 lines is the defended sweet spot. The Prickles 15-line cap is two orders of magnitude tighter — a deliberate editorial position. The number is wider than the canon's, and that is the editorial decision worth defending: McConnell summarises an industry; the canon picks a position. The Prickles cap of 15 lines is two orders of magnitude tighter than McConnell because the author would rather have a function that is too small than a function that is too big.
Quotes
The particular upper bound that has been used for cyclomatic complexity is 10 which seems like a reasonable, but not magical, upper limit.
If you need more than three levels of indentation, you're screwed anyway, and should fix your program.
Restrict the length of any function to what can be printed on a single sheet of paper in a standard reference format with one line per statement and one line per declaration.
Make functions short. The studies show that routines longer than two hundred lines are more prone to errors. Smaller routines are easier to test, easier to read, and easier to change.
Evidence
Twenty external sources, ranked by author authority. The first five are the canon; expand to see the rest, including the qualifiers and the named opposers. Each links out to its primary source.
- 01The foundational paper. Cyclomatic number is a graph-theoretic property of a function's control-flow graph; ≤10 is McCabe's recommended cap. Half a century of static analysis derives from this.
- 02Function-length cap (Rule 4) at one printed page; restricted control flow (Rule 1) bans `goto`, `setjmp`, recursion. Ten rules used on Mars Exploration Rover flight software; the discipline is universal.
- 03Linux kernel coding style §1, §6SupportsThree indentation levels max; functions do one thing; the contributor guidelines bounce the patch before review. The most-read style guide in production use.
- 04Summarises the empirical research: 65–200 lines is the defended sweet spot. The Prickles cap of 15 is deliberately tighter; the qualification is that the canon is taking a position, not citing the average.
- 05Clean Code, ch. 3 “Functions”Supports“The first rule of functions is that they should be small. The second rule of functions is that they should be smaller than that.” The most-quoted single-sentence statement of the rule.
Sixteen sources, three stances. The supporters cluster on the metrics themselves: McCabe, Holzmann, Torvalds, McConnell, Martin. The qualifiers further down carry the “short functions are good but the cut decides where” reading. The opposers argue that depth, not length, is the load-bearing variable. The honest argument is between the cap and the depth view; the canon's answer is to ship both.
Examples
// Before: one 25-line function. v(G) = 6, max-depth = 3, max-lines-per-function = 15.function decideHedgehogHibernation(weatherC: number, weightG: number, foodG: number): boolean { if (weatherC < 5) { if (weightG < 600) { if (foodG < 50) { return false; } return true; } } return false;}
// After: named predicates. The top-level decision reads top-to-bottom.function isWinterStarvation(hedgehog: Hedgehog): boolean { return hedgehog.weatherC < 5 && hedgehog.foodG < 50;}function isReadyToHibernate(hedgehog: Hedgehog): boolean { return hedgehog.weatherC < 5 && hedgehog.weightG >= 600;}function shouldHibernate(hedgehog: Hedgehog): boolean { if (isWinterStarvation(hedgehog)) return false; return isReadyToHibernate(hedgehog);}
Enforcement
Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| complexity | ESLint core | the McCabe cyclomatic number per function. Default 20; the Prickles repo sets it at 3 — McCabe's metric tightened by a factor of three. |
| max-lines-per-function | ESLint core | function length above the configured cap. Pair with `skipBlankLines: true` and `skipComments: true` so the cap measures statements, not whitespace. |
| max-depth | ESLint core | indentation depth above the configured cap — the lever Linus names directly. Three is the kernel's number; the Prickles repo agrees. |
| max-statements | ESLint core | statement count per function. The complement to length: a function can be short on lines but long on statements when arguments wrap. |
| max-nested-callbacks | ESLint core | callback nesting depth. The async-shaped variant of `max-depth`; catches Promise-then chains and event-handler pyramids. |
| max-params | ESLint core | parameter count above the configured cap. Pairs with S3 Parameter Object; the lint forces the bundling conversation. |
| sonarjs/cognitive-complexity | eslint-plugin-sonarjs | the depth-weighted complement to McCabe. SonarSource's reformulation penalises nesting more heavily than control flow; useful where McCabe under-counts. |
eslint.config.mjsconfiguration snippet
import tseslint from 'typescript-eslint';
import sonarjs from 'eslint-plugin-sonarjs';
export default tseslint.config({
files: ['**/*.{ts,tsx}'],
plugins: { sonarjs },
rules: {
complexity: ['warn', 3],
'max-lines-per-function': ['warn', { max: 15, skipBlankLines: true, skipComments: true }],
'max-lines': ['warn', { max: 150, skipBlankLines: true, skipComments: true }],
'max-depth': ['warn', 3],
'max-statements': ['warn', 10],
'max-nested-callbacks': ['warn', 3],
'max-params': ['warn', 3],
'sonarjs/cognitive-complexity': ['error', 8],
}
});AI rules
.cursor/rules/s2-cyclomatic-caps.mdc---
description: Prickles S2 — Cyclomatic Caps
globs: "**/*.{ts,tsx,js,jsx,py,java,php}"
alwaysApply: false
---
## Prickles S2 — Cyclomatic Caps
Cap cyclomatic complexity per function. The number is editorial; pick one and put it in the linter.
Cap function length. Pick a number; the canonical Prickles default is 15 lines. Pair with caps on indentation depth, statement count, parameter count.
When a function trips the cap, refactor first — extract a helper, lift a guard clause, split the responsibility. Do not raise the cap to suit the function.
Refuse to disable the rule per occurrence. The cap is not negotiable per pull request; if the cap is wrong for the project, change the cap globally and document why.Repo layout, CI, and ESLint wiring for these paths live on /implementation — not repeated on every tenet.
Counter-argument
The strongest steelman is John Ousterhout's, sharpened by a decade of teaching A Philosophy of Software Design.5A Philosophy of Software Design, 2nd ed. (2021). Ch. 7 “Different Layer, Different Abstraction” argues that depth — not length — is the load-bearing variable. The strongest steelman against blanket function-length caps. Ousterhout argues that the unit that matters is not the function but the module, and that aggressive function-splitting produces shallow modules whose cumulative complexity is higher than the long function would have been. A 60-line function with one entry, one exit, and a clear story can be easier to read than seven 8-line helpers whose only common ancestor is a file path. The cap, applied without judgement, optimises for a metric that is not what makes code easy to read.
Counter-argument retort
Ousterhout is right that depth matters more than length, and the canon does not argue with him on that point. Where the canon parts company is on what the linter should enforce. Depth is hard to measure; length and cyclomatic complexity are trivial. Pick the metric the linter can act on; let F1 Single Responsibility and the code-review conversation handle the depth question after the cap has done its work.5A Philosophy of Software Design, 2nd ed. (2021). Ch. 7 “Different Layer, Different Abstraction” argues that depth — not length — is the load-bearing variable. The strongest steelman against blanket function-length caps.
The shallow-module objection — seven 8-line helpers worse than one 60-line function — is real and avoidable. The cap is not a mandate to extract; it is a mandate to think when the function gets long. If the right answer is to leave the function long, the function gets a comment that says so and the cap gets an exception that says why. The default is the cap; the exception is the audited deviation.
The genuine residue is the YAGNI-purist's reply: a 15-line cap forces premature extraction, the helpers are speculative, the cost surfaces six months later. The classic counter from Fowler and Metz was the rule of three — wait for the third caller before naming the abstraction. I disagree. With AI agents catching duplication on the second occurrence, the cost of waiting compounds faster than the cost of an early name. Extract on the second; rename the helper if the third caller reshapes it. The cap forces decomposition; F3 DRY says the second instance is the trigger.
The number is editorial. McCabe says 10; the kernel says three indentation levels; NASA says one printed page; Prickles says complexity 3 and length 15. The number is not the principle; the principle is that the number lives in the linter and is not negotiable per pull request. Hard limits beat soft conventions because the soft conventions get worn down. Pick a number and stop arguing.
Notes
- [1]Prickles repo (eslint.config.mjs) — complexity: ['warn', 3], max-lines-per-function: ['warn', { max: 15, skipBlankLines: true, skipComments: true }], max-depth: ['warn', 3], max-statements: ['warn', 10]. The cap is the lever; the linter is the surface.
- [2]Tom McCabe — “A Complexity Measure”, IEEE Transactions on Software Engineering SE-2(4) (December 1976), pp.308–320. The foundational paper. Defines v(G) as the cyclomatic number of the control-flow graph; recommends v(G) ≤ 10 as “a reasonable, but not magical, upper limit”.
- [3]Gerard J. Holzmann (NASA/JPL) — “The Power of Ten — Rules for Developing Safety-Critical Code” (2006). Ten rules for code that flies on Mars; the function-length cap (Rule 4) and the restricted-control-flow rule (Rule 1) are the load-bearing two for this tenet.