Code complexity measures how hard a function is to test or to read. A linter works it out for each function. There are two common measures. Cyclomatic complexity, from Thomas McCabe’s 1976 paper, counts the independent paths through a function. Cognitive complexity, from G. Ann Campbell’s white paper for SonarSource, scores how hard the function is to follow and adds extra for each level of nesting.

Neither number means much without the tool that produced it. ESLint, Ruff, PMD and PHP_CodeSniffer all report cyclomatic complexity, and each of them counts different things. The Prickles packs cap it at 3 in every language. Every tool’s counting rules and defaults below were checked against its own documentation or source code on 12 October 2026.

McCabe code complexity counts paths

McCabe’s A Complexity Measure (IEEE Transactions on Software Engineering, 1976) draws a function as a graph of its control flow. It measures the function by its decisions, whatever its length. The working method is in Structured Testing, a 1996 NIST guide that McCabe wrote with Arthur Watson:

  • Start at 1, for the function itself.
  • Add 1 for each decision with two outcomes, such as an if, a loop or a case.
  • Add 1 for each boolean operator that short-circuits, such as && in C, because it can skip code.

That total counts the independent paths through the function. Structured testing, the guide’s method, needs one test path for each of them. The guide calls 10 McCabe’s original limit, with strong evidence behind it, and notes that limits as high as 15 have also worked.

S2 Cyclomatic Caps covers cyclomatic complexity in more depth, with the case for capping it.

What cognitive complexity adds

Campbell’s Cognitive Complexity (SonarSource, version 1.7, 2023) starts from a complaint about McCabe’s number. It was meant to measure how testable and how maintainable code is, and Campbell argues that it measures the first well and the second poorly. Her measure scores a method by three rules:

  • Ignore shorthand that lets several statements read as one, such as a?.b in place of a null check.
  • Add 1 for each break in the straight run of the code, such as a loop, an if, a ternary or a catch.
  • Add 1 more for each level of nesting that the break sits inside.

Two differences from McCabe follow. A switch costs 1 however many cases it has, where cyclomatic complexity adds 1 for each case. A run of the same boolean operator, such as a && b && c, costs 1 in total.

Campbell’s opening example shows the gap with two methods that both score 4 for cyclomatic complexity. One is a switch with three cases and the other a pair of nested loops, and for cognitive complexity they score 1 and 7. SonarSource’s ESLint plugin flags a function above 15 by default, and PMD’s version of the rule flags 15 or more.

Cognitive vs cyclomatic, side by side

Cyclomatic and cognitive complexity differ most over nesting. Both versions below make the same decision. Cyclomatic complexity scores both of them 3, because each has two decisions. Cognitive complexity scores the nested version 3 and the flat version 2, because the inner if sits one level deep and costs 2.

Nested if (order.paid) { if (order.express) { return EXPRESS_DAYS; } } return STANDARD_DAYS; +1 +2 nested once Cyclomatic 3 Cognitive 3 Guard clause if (!order.paid) { return STANDARD_DAYS; } return order.express ? EXPRESS_DAYS : STANDARD_DAYS; +1 +1 Cyclomatic 3 Cognitive 2
Both versions have the same paths, so cyclomatic complexity gives them the same score. Cognitive complexity charges the nested if for its depth.

Running ESLint’s complexity rule and the cognitive-complexity rule from eslint-plugin-sonarjs on both versions gave these scores. Neither score fails the nested version, but the TypeScript, JavaScript and Python packs reject it anyway. Two rules in the TypeScript and JavaScript packs, sonarjs’s no-collapsible-if and unicorn’s no-lonely-if, reject an if whose only content is another if. Ruff’s SIM102 does the same in Python.

S4 Guard Clauses asks for this flat shape. Its precondition returns early, so the main work runs at one level. Its last check is a ternary, because the TypeScript and JavaScript packs reject a guard that only the final return follows.

One function, six scores

Here’s one function in each pack’s language. It picks a delivery time in days from two flags on an order.

Viewing: TypeScript.
interface Order {
  readonly express: boolean;
  readonly priority: boolean;
}

const EXPRESS_DAYS = 1;
const STANDARD_DAYS = 3;

export const deliveryDays = (order: Order): number =>
  order.express || order.priority ? EXPRESS_DAYS : STANDARD_DAYS;

Each pack’s cyclomatic rule scores it differently. The two cognitive complexity rules disagree as well. Every score passes its pack’s limit.

Rule Score What it counts here First score it reports
ESLint complexity (TypeScript, JavaScript) 3 One for the function, plus ?: and || 4
PMD CyclomaticComplexity (Java) 3 One for the method, plus ?: and the || in its condition 4
PHP_CodeSniffer CyclomaticComplexity (PHP) 2 One for the function, plus ?: 4
Ruff C901 (Python) 1 Only the function itself 4
eslint-plugin-sonarjs cognitive-complexity (TypeScript, JavaScript) 1 Only the ?: 16
PMD CognitiveComplexity (Java) 2 ?: and || 15

The ESLint scores come from running both rules on the TypeScript and JavaScript versions. The Ruff, PMD and PHP_CodeSniffer scores were counted by hand, following each tool’s source code as released on 12 October 2026. The cyclomatic scores differ because each rule has its own idea of a path:

  • ESLint’s complexity counts the most. On top of the usual statements, its source adds 1 for &&, || and ?? and for logical assignments such as ||=. It also adds 1 for each optional ?. and each default value.
  • Ruff’s C901 adds 1 for each if, elif, loop, case, except and nested function. Its source counts no boolean operators and no x if a else y expressions. Ruff’s PLR0912, which the Python pack also sets to 3, counts no branches here at all.
  • PMD’s CyclomaticComplexity counts && and || only inside the condition of an if, a loop, a switch or a ?:, according to its source. So return a && b; adds nothing, and each throw adds 1.
  • PHP_CodeSniffer’s sniff counts ?:, ??, ??=, ?->, each match arm and a switch’s default as well as its cases. It doesn’t count && or ||.

NIST’s guide says a boolean operator that short-circuits adds 1. By that rule Ruff and PHP_CodeSniffer leave paths out, because and and or in Python, and && and || in PHP, all short-circuit.

Cognitive complexity rules split for a different reason. Campbell’s paper charges 1 for each run of like boolean operators, and PMD’s version follows it. SonarSource’s own rule for JavaScript and TypeScript stopped charging for || and ?? in October 2024, in pull request 4862, so only runs of && count there now.

Small changes move the numbers further apart. Give the TypeScript version a per-order fallback, order.standardDays ?? STANDARD_DAYS, and ESLint scores it 4 and fails it, while its cognitive score stays at 1. A one-line function returning order.carrier?.days ?? STANDARD_DAYS already scores 3 in ESLint and 0 for cognitive complexity.

Code complexity limits in each Prickles pack

Every pack caps cyclomatic complexity at 3, well below McCabe’s 10, and S2 makes the case for a hard cap. The settings differ because the tools draw the line in different places.

Pack Cyclomatic rule Setting Cognitive rule
TypeScript ESLint complexity 3 eslint-plugin-sonarjs cognitive-complexity, 15
JavaScript ESLint complexity 3 eslint-plugin-sonarjs cognitive-complexity, 15
Python Ruff C901 max-complexity, with PLR0912 max-branches 3 each None
Java PMD CyclomaticComplexity methodReportLevel 4 PMD CognitiveComplexity, at its default of 15
PHP PHP_CodeSniffer Generic.Metrics.CyclomaticComplexity 3 None

Out of the box the tools allow much more. ESLint’s default is 20. PMD’s docs and Ruff’s settings reference both give 10. PHP_CodeSniffer warns above 10 and reports an error above 20.

The TypeScript, JavaScript and PHP packs also cap nesting at 3 levels. The Java pack caps nested if statements at 3. In PHP, that nesting cap covers part of what a cognitive rule would catch. The Python pack has neither: Ruff’s nesting rule, PLR1702, still needs Ruff’s preview mode. The pack leaves preview mode off. Each pack’s rules for long functions and other code smells sit beside these.

What tight limits do to a coding agent

With a cap of 3, a coding agent has to split every function that trips it. The quickest split cuts by size. That doesn’t always remove any complexity.

Priyansh Trivedi and Olivier Schmitt of SonarSource measured the effect in Does Code Cleanliness Affect Coding Agents? (2026). They ran Claude Sonnet 4.6 in Claude Code on pairs of repositories that behave the same but differ in static-analysis issues and cognitive complexity. Across 660 runs, the agent used 7 to 8% fewer tokens on the cleaner code and passed the same share of tasks.

On the 13 tasks routed through complexity hotspots, the cleaner versions saved no input tokens. On those tasks the agent:

  • used 1.8% more input tokens
  • read 11.2% more files
  • went back to files it had already read 20.2% less often

In the authors’ words, “extraction redistributes complexity across more methods rather than eliminating it”. The study used one model, and its authors built every pair of repositories and wrote every task. It reports no significance tests. The article on AI token cost covers the wider evidence on code quality and tokens.

That result cuts against the canon’s own caps, because they push code into small functions. The canon guards only against the emptiest extraction. S5 Inline-First rejects a one-line helper with one caller and a name no clearer than its body. A larger helper, cut only to get under the number, passes S5 and can still spread the complexity around.

F1 Single Responsibility gives a better place to cut. It splits a function along its responsibilities, so each piece has one reason to change and a name that says what it does. The cap says when to split a function, and F1 says where.

How to set code complexity limits that work

  1. Put the number in the linter config, so a function over it fails the build. S2 Cyclomatic Caps calls the exact number editorial.
  2. Check whether your tool reports at the number or above it, and set it so that it enforces the limit you mean.
  3. Read what your tool counts before you compare scores across languages or teams.
  4. Cap nesting depth too, or run a cognitive complexity rule beside the cyclomatic one, because cyclomatic complexity doesn’t see nesting.
  5. When a function trips the cap, lift its preconditions into guard clauses, then split what’s left by responsibility.
  6. Keep a one-line helper with one caller inline, even when extracting it would get the function under the number.
  7. If the cap is wrong for the project, change it for the whole project and write down why, instead of switching the rule off for one function.

Each pack’s full config is on the implementation page.