Clean code principles are the habits that keep code easy to read and change. They ask for small functions, names that say what things are for, no duplication, few parameters and no comments where the code could speak for itself. They’re usually tied to Robert C. Martin’s book Clean Code (2008).

Some of those principles can be counted, so a linter can enforce them. Function length, complexity, nesting depth, parameter count, copied code and magic numbers all come down to numbers a tool can check on every run. The rest need judgement. They belong in code review. Each countable principle below comes with its lint rule in five languages. Where the Prickles canon parts company with the book comes last.

A linter enforces A reviewer judges Lines a function 15 Names that say why Complexity 3 Where to split Nesting depth 3 When to abstract Parameters 3 What comments say Copied code none A pattern’s cost Magic numbers none Tidying as you go every run, as an error in review Clean code
Principles you can count go in the linter and are checked on every run. The rest need a reviewer.

What is clean code?

Martin’s first chapter asks the question under that heading (page 7), and answers it with other people’s words first. He collects definitions from six experienced programmers, among them Bjarne Stroustrup, Grady Booch, Michael Feathers and Ward Cunningham. Their answers overlap. Clean code does one thing well, reads plainly, has tests and turns out to be what the reader expected.

Martin’s own argument is about reading. He puts the time spent reading code at well over ten times the time spent writing it (page 14). So code that’s easy to read is also quicker to change. Readable code is what he’s after, and each principle is a way of getting there.

He also presents the book as one view among several. Martin, who calls himself Uncle Bob, describes it as one school of clean code (pages 12 and 13). He says no school is absolutely right and anticipates readers disagreeing with some of it. The Prickles canon is another school, with its differences from the book in the last section.

Clean code principles a linter can enforce

A principle can go in a linter when it can be counted. The table pairs each countable principle with where the book makes its case, its Prickles tenet and the canon’s limit.

Principle In Clean Code Tenet Limit in the canon
Small functions Chapter 3, “Small!” (page 34) F1 Single Responsibility Principle and S2 Cyclomatic Caps 15 lines a function and a complexity of 3
Shallow nesting Chapter 3, “Blocks and Indenting” (page 35) S4 Guard Clauses 3 levels
Few arguments Chapter 3, “Function Arguments” (page 40) S3 Parameter Object 3 parameters
No duplication Chapter 3, “Don’t Repeat Yourself” (page 48) F3 Don’t Repeat Yourself and TA2 Duplication Detection No copied block that the duplication check finds
Named constants Chapter 17, heuristic G25 (page 300) S5 Inline-First Every number except -1, 0 and 1 has a name

Complexity here means cyclomatic complexity, a count of the separate paths through a function. A function with no branches scores 1, and each decision point, such as an if or a loop, adds one. A limit of 3 allows two decisions.

In the default layout of every Prickles pack, the agent always loads an entry file that carries one rule line for each tenet. So an agent working in the repository has these lines in every session. Here they are word for word, from release 3.0.0 of the canon:

  • F1 Single Responsibility Principle: One obvious job per function, class, or module. When a second concern shows up in the same unit, peel it out so each piece has its own reason to change.
  • S2 Cyclomatic Caps: Cap cyclomatic complexity per function. The number is editorial. Pick one and put it in the linter.
  • S4 Guard Clauses: When a function’s preconditions are nested as conditionals, lift each one to a guarded early return at the top of the function.
  • S3 Parameter Object: Cap function parameters at three. The fourth parameter is the missing struct.
  • F3 Don’t Repeat Yourself: Give every fact, rule and decision in the system one home. If you find yourself typing the same logic, the same literal or the same shape twice, the second copy is a bug looking for a deadline.
  • TA2 Duplication Detection: Run jscpd in CI for every commit. Threshold zero on greenfield code. Directory-scoped overrides for legacy zones with a paydown plan.
  • S5 Inline-First: Inline first. Extract when a name is needed. A binding justifies itself when its name carries information the expression does not.

A rule line tells the agent what to do. The linter checks that it did. TA1 Linter as Law makes the check binding: ‘Make every quality gate an error, never a warning. A warning gets ignored within a sprint. An error stops the build.’

That check matters more when an agent writes the code. A linter run after each change reports every breach with a file and a line number. A limit then doesn’t depend on the agent remembering a sentence from its instructions. Two studies bear on this:

Study What it tested What it found
John Yang and colleagues’ SWE-agent (2024) GPT-4 Turbo on the 300 tasks of SWE-bench Lite, each a real issue from a Python project on GitHub, with and without a lint check on every edit 18.0% of tasks resolved with the check, against 15.0% without it
Priyansh Trivedi and Olivier Schmitt’s Does Code Cleanliness Affect Coding Agents? (Sonar, 2026) Claude Code, running Claude Sonnet 4.6, on cleaner and messier versions of otherwise matching repositories 7 to 8% fewer tokens on the cleaner code, with a similar pass rate

Both have limits. SWE-agent’s linter looked for faults such as syntax errors, undefined names and broken indentation. So it shows that agents act on lint feedback, and says nothing about style. Sonar’s authors built every repository pair themselves and report no significance tests, so treat their figure as a first measurement.

Lint rules in five languages

Rules in the TypeScript, JavaScript and Python packs

Principle ESLint (TypeScript and JavaScript) Ruff and Pylint (Python)
Function size max-lines-per-function 15 and max-statements 10 PLR0915, 10 statements
Complexity complexity 3 C901 3 and PLR0912, 3 branches
Nesting max-depth 3 No depth limit, though the RET rules include RET505 to flag an else after a return
Parameters max-params 3 PLR0913 3
Duplication sonarjs/no-identical-functions in each file, and jscpd across files Pylint’s duplicate-code, and jscpd across files
Magic numbers @typescript-eslint/no-magic-numbers in TypeScript, no-magic-numbers in JavaScript PLR2004, for numbers in comparisons
Comments prickles/no-comments, the pack’s own rule. It fails every comment but a few kinds Pylint, through the pack’s prickles_no_comments.py plugin. It fails every comment and docstring but a few kinds

Rules in the Java and PHP packs

Principle PMD and Checkstyle (Java) PHP_CodeSniffer with PHPMD and phpmnd (PHP)
Function size PMD’s NcssCount 15, and Checkstyle’s MethodLength 15 PHPMD’s ExcessiveMethodLength 15
Complexity PMD’s CyclomaticComplexity, set to 4 Generic.Metrics.CyclomaticComplexity 3
Nesting PMD’s AvoidDeeplyNestedIfStmts, set to 4 Generic.Metrics.NestingLevel 3
Parameters PMD’s ExcessiveParameterList set to 4, and Checkstyle’s ParameterNumber 3 PHPMD’s ExcessiveParameterList 3
Duplication PMD’s copy-paste detector (CPD) at 50 tokens CPD at 50 tokens
Magic numbers PMD’s AvoidLiteralsInIfCondition, and Checkstyle’s MagicNumber phpmnd, the PHP Magic Number Detector
Comments PMD’s CommentContent, set to fail every comment, Javadoc included, but a few kinds NoCommentsSniff.php, the pack’s own sniff. It fails every comment but a few kinds

Checkstyle is optional in the Java pack. The pack lists the checks to pair it with. From release 3.0.0, every pack fails on any comment except tool directives such as eslint-disable or # noqa, SPDX licence lines and generated-file banners. The JavaScript and PHP packs also let through a docblock that holds type tags alone. A rule can remove a comment, but it can’t make the code explain itself. Comments come up again in the next section for that reason.

Clean code principles that need judgement

TA1 Linter as Law draws this line itself. It names pattern misuse, naming taste and decomposition judgement as places a linter can’t reach, and hands them to agent rules, code review and tests. These principles sit on that side of the line:

  • Names. Linters check a name’s form, such as kebab-case file names like order-total.ts in TypeScript and JavaScript, or Python’s PEP 8 names through Ruff’s N rules. PMD, PHP_CodeSniffer and PHPMD check their languages’ naming conventions. None of them can tell whether a name is true. F2 Intention-Revealing Names asks for more: ‘Use names that answer why this exists and what it does in the domain vocabulary. If the signature does not hold the meaning, rename.’
  • Where to split a function. A length limit says a function is too long, but not where its seam is. F1 Single Responsibility Principle asks for one reason to change per unit, and only a reader can see the reasons.
  • When to add an abstraction. F4 Simplicity: ‘Build for the requirement at hand, not the speculative future. No line of code is cheaper than the one you didn’t write, and speculative generality is debt with no creditor.’
  • What the code says without comments. The packs fail on a comment, but only a reader can tell whether the names, types and tests carry what it would have said. F5 Self-Documenting Code: ‘Do not add comments that explain what the code does. If a reader needs help, improve names, extract functions, add types, or add tests until the code tells the story.’
  • What a class shows. The packs check part of F6 Encapsulation. PHP makes every property and method declare its visibility, and TypeScript and JavaScript forbid reassigning a parameter. Whether a field should be visible at all is still a reviewer’s call.
  • Tidying as you go. Martin’s Boy Scout Rule asks you to check code in a little cleaner than you checked it out (page 14). P6 Leave it Better Than You Found it keeps the tidying small: ‘Tidy the line you touched. The unit is small: rename one variable, split one function, delete one dead import. The trigger is incidental: tidy only the file you opened to change.’

PHP’s pack runs PHPMD’s ruleset called Clean Code, and its BooleanArgumentFlag rule reports a parameter that defaults to true or false. The rule’s source checks only for that default, so a bool parameter without one passes. Martin lists flag arguments as a smell in chapter 17 (page 288), yet the other four packs have no rule for them at all. A function with one passed the TypeScript and JavaScript packs’ rules in a test on 12 October 2026.

Everything in this section goes to review. AI code review has a checklist for that, with one line per tenet. Code smells pairs each smell with a lint rule to catch it, where one exists.

A clean code example a linter can’t judge

Both versions below pass every lint rule in their pack. All five languages were run on 12 October 2026. The TypeScript and JavaScript versions were also checked against those packs’ rules. The first version gives the reader nothing to go on. handle, data and fix could mean anything, and fix is a one-line helper with one caller and a name no clearer than its body.

Viewing: TypeScript.
const fix = (value: string): string => value.trim().toLowerCase();

export const handle = (data: readonly string[]): string[] => data.map((item) => fix(item));

Version two names what it does in the words of its domain. Its helper has gone back into the one place it was used:

Viewing: TypeScript.
export const normaliseTags = (tags: readonly string[]): string[] =>
  tags.map((tag) => tag.trim().toLowerCase());

No lint rule asks for this change. F2 Intention-Revealing Names asks for the names, and S5 Inline-First for folding the helper back in. Both versions return the same list. In PHP the tags arrive as a variadic string ...$tags, typing each one without a docblock.

Where Prickles parts company with the book

Martin anticipates readers disagreeing with parts of the book. The canon disagrees in three places.

Extracting small functions

Martin makes his case for size in “Small!”, the section starting on page 34. A function, he says, should hardly ever reach 20 lines. John Ousterhout and Martin argued the point between September 2024 and February 2025, in an exchange published as A Philosophy of Software Design vs Clean Code. Ousterhout says splitting that hard produces shallow methods, which hide little behind their interface, and entangled ones that have to be read together. Martin agrees the first edition said little about when to stop, and says the second edition is more balanced. Then he gives his default:

Still, if I must err, I’d rather err on the side of decomposition.

— Robert C. Martin, A Philosophy of Software Design vs Clean Code, 2025

Prickles caps a function at 15 lines, so it agrees that functions stay small. Its default runs the other way, though. S5 Inline-First keeps an expression where it is until it needs a name: ‘Inline first. Extract when a name is needed.’

Sonar’s study points the same way. On its track of complexity hotspots, the cleaner versions cost 1.8% more input tokens and the agent read 11.2% more files. The authors put that down to extraction spreading complexity across more methods rather than removing it. The canon splits a function when it has a second responsibility, as F1 Single Responsibility Principle sets out. A line count can’t see one.

TODO comments

Martin’s chapter on comments sorts them into good and bad. The canon agrees with him about one of his good kinds and not another:

Comment Clean Code, chapter 4 Prickles
A TODO, FIXME, XXX or HACK comment TODO comments are among the good ones (page 58) An error in every pack, like any comment, and named as a TODO in TypeScript and JavaScript by ESLint’s no-warning-comments
A licence header Legal comments are among the good ones (page 55) Allowed by F5 Self-Documenting Code without debate

F5’s only other exceptions are generated-file banners and narrow linter suppressions. Ousterhout disagrees with Martin and the canon alike. In the same exchange, he argues that comments are essential for recording what isn’t obvious from the code. He trusts them by default, where Martin checks them against the code first.

Patterns

Chapter 17’s heuristic G23 is titled “Prefer Polymorphism to If/Else or Switch/Case” (page 299). PT3 Every Pattern Has a Failure Mode takes no pattern on trust, whatever it replaces: ‘Before introducing a pattern, read its Consequences (or Cons) section in GoF / Refactoring Guru / the canonical reference. Name the cost out loud in the same paragraph as the choice.’ GoF means the Gang of Four’s book Design Patterns, where each pattern has a Consequences section.

For the Visitor pattern, PT3 goes further. A discriminated union is a type whose values each hold a tag naming their variant. Where a language has them, PT3 prefers one that the compiler checks for missing cases. So it accepts a switch where G23’s title asks for polymorphism.

How to enforce clean code principles

  1. Put each countable limit from the tables above in your linter’s config, as an error rather than a warning.
  2. Run the linter on every commit and in CI, and have your coding agent run it after each change.
  3. When a function trips a limit, change the function and leave the limit alone, as S2 Cyclomatic Caps says.
  4. Give your agent the rule lines in its always-loaded instructions, so it knows the reason as well as the limit.
  5. Review the rest with a checklist of one line per principle, and name the principle in each finding.

Prickles builds these lint configs for each language, plus the rule files for each agent, on the implementation page.