A code smell is a sign in the code that something underneath may need to change. Martin Fowler’s CodeSmell (2006) describes it as something you notice on the surface that usually points to a deeper problem in the system. The smell tells you where to look, and what you find there decides whether anything changes.

Seven common code smells have lint rules in the Prickles packs: long functions, long parameter lists, deep nesting, complex conditionals, duplicated code, large classes and magic numbers. Each gets its usual fix below, with a Prickles tenet arguing for that fix and a rule in each Prickles lint pack to fail the build. The rest of the list needs someone, or an agent, to read the code.

Code smells A lint rule fails A reviewer spots Long function Long parameter list Deep nesting Complex conditional Duplicated code Large class Magic number Mysterious name Feature envy Shotgun surgery Divergent change Primitive obsession Data clumps Message chains
Seven common smells have a lint rule in the Prickles packs. The rest of the list, shown here in part, needs a reviewer.

Every rule and setting here comes from the TypeScript, JavaScript, Python, Java and PHP packs on the implementation page, as of Prickles release 3.0.0. Each tool’s documentation was checked on 12 October 2026.

Where the code smells list comes from

Kent Beck coined the term while helping Fowler with his book on refactoring, according to the same CodeSmell entry. The list itself is chapter 3 of Refactoring: Improving the Design of Existing Code, “Bad Smells in Code”, which the two wrote together. The first edition came out in 1999 and the second in 2018.

In the second edition the chapter names 24 smells, from Mysterious Name and Duplicated Code to Comments, as InformIT’s excerpt of the chapter (2018) shows. Fowler’s Changes for the 2nd Edition of Refactoring (2018) says the chapter had a thorough overhaul. Each smell points to refactorings in the book’s catalogue, online at refactoring.com.

Fowler and Beck give no precise criteria for when to refactor. Chapter 3 says why: “In our experience, no set of metrics rivals informed human intuition.”

A lint rule can’t run on intuition, so the Prickles canon picks the numbers. S2 Cyclomatic Caps calls the number editorial and says to pick one and put it in the linter. F1 Single Responsibility Principle calls the caps “linter smoke alarms” that catch tangles early, and says they never replace asking who pays when the code changes.

Seven code smells and the usual fix for each

Four of the seven are headings in Fowler and Beck’s chapter. Deep nesting, complex conditionals and magic numbers aren’t, though the catalogue has a refactoring for each.

Smell What it looks like Usual fix Tenet
Long function A function you can’t read in one go, doing several jobs Extract Function F1 Single Responsibility Principle
Long parameter list Four or more parameters, some of which travel together Introduce Parameter Object S3 Parameter Object
Deep nesting An if inside an if, with the main path at the bottom Replace Nested Conditional with Guard Clauses S4 Guard Clauses
Complex conditional Many paths through one function Decompose Conditional, or split the function S2 Cyclomatic Caps
Duplicated code Logic repeated in two or more places Extract Function, called from each place F3 Don’t Repeat Yourself and TA2 Duplication Detection
Large class A class or file that has taken on several jobs Extract Class F1 Single Responsibility Principle
Magic number A number whose meaning the reader has to guess Replace Magic Literal S5 Inline-First

Which lint rule catches each smell

Every pack sets each rule as an error, so the build fails when one trips, as TA1 Linter as Law asks.

Smell TypeScript and JavaScript Python Java PHP
Long function max-lines-per-function 15 Ruff and Pylint max-statements 10 PMD NcssCount 15 PHPMD ExcessiveMethodLength 15
Long parameter list max-params 3 Ruff and Pylint max-args 3 PMD ExcessiveParameterList 4 PHPMD ExcessiveParameterList 3
Deep nesting max-depth 3 No limit set PMD AvoidDeeplyNestedIfStmts 4 PHP_CodeSniffer Generic.Metrics.NestingLevel 3
Complex conditional complexity 3 Ruff C901 3 and max-branches 3 PMD CyclomaticComplexity 4 PHP_CodeSniffer Generic.Metrics.CyclomaticComplexity 3
Duplicated code jscpd with --threshold 0 jscpd with --threshold 0 PMD’s CPD, 50 tokens PMD’s CPD, 50 tokens
Large class max-lines 150 a file Pylint max-public-methods 7 PMD NcssCount 300 a class PHPMD ExcessiveClassLength 150
Magic number no-magic-numbers, or @typescript-eslint/no-magic-numbers in TypeScript Ruff PLR2004 PMD AvoidLiteralsInIfCondition phpmnd
  • PMD reports a method that reaches the number it’s given, as its design rules set out. So for complexity, nesting and parameters, the Java pack sets 4 to allow the canon’s 3.
  • The Python pack sets no nesting limit of its own. Its complexity caps of 3 leave a function little room to nest.
  • The Java and PHP packs name PMD’s copy-paste detector, CPD, to run beside the lint with a minimum of 50 tokens.

Long functions and large classes

Fowler and Beck’s fix for a long function is nearly always Extract Function. It moves a group of lines that does one thing into a function named after it. A class that does too much tends to collect fields, and Fowler and Beck fix it with Extract Class.

F1 Single Responsibility Principle asks for one obvious job per function, class or module. The packs measure size in different units. TypeScript, JavaScript and PHP allow 15 lines a function, and TypeScript and JavaScript also allow 10 statements. Python allows 10 statements a function, and PMD’s NcssCount in Java counts statements too.

Long parameter list

A call such as confirmationFor(hotelName, guestName, arrival, nights) passes four values, and three of them describe one stay. Fowler and Beck call a group of values that keeps turning up together a data clump, and suggest giving it a home of its own.

Introduce Parameter Object fixes it. S3 Parameter Object caps a function at three parameters, so the fourth fails max-params or its equivalent in each pack. The stay becomes one type:

Viewing: TypeScript.
type Stay = Readonly<{ guestName: string; arrival: string; nights: number }>;

export const confirmationFor = (hotelName: string, stay: Stay): string =>
  `${stay.guestName}: ${stay.nights} nights at ${hotelName} from ${stay.arrival}`;

confirmationFor takes two parameters now. The next detail about a stay goes into Stay instead of onto the parameter list of every function that handles one.

Deep nesting and complex conditionals

Nested conditions push the main path of a function to the right, under every check that comes before it. ESLint’s max-depth limits how deep blocks can nest, and its complexity rule counts cyclomatic complexity, the number of independent paths through the code.

Nested conditions if (paid) { if (has items) { return ready to send } else { return nothing to send } } else { return awaiting payment } 2 blocks deep Guard clause if (not paid) { return awaiting payment } return no items ? nothing to send : ready to send top level
A guard clause returns early, so the main path finishes at the top level of the function.

Replace Nested Conditional with Guard Clauses fixes it. Each condition is tested at the top and returns early when it fails, so the main path runs unindented. S4 Guard Clauses argues for it. S2 Cyclomatic Caps sets a cap of 3, leaving room for two decision points. The sample spends one on a guard and one on a ternary, because the TypeScript and JavaScript packs reject a guard that only the final return follows. More checks than that belong in a function of their own. Fowler’s catalogue calls that move Decompose Conditional.

Viewing: TypeScript.
type Order = Readonly<{ paid: boolean; items: readonly string[] }>;

export const dispatchStatus = (order: Order): string => {
  if (!order.paid) {
    return "Awaiting payment";
  }
  return order.items.length === 0 ? "Nothing to send" : `Ready to send ${order.items.join(", ")}`;
};

Duplicate code detection

Duplicated Code comes second in the chapter’s list. Fowler and Beck fix it by extracting a function and calling it from each place. F3 Don’t Repeat Yourself says to extract on the second copy, inside one bounded context, without waiting for a third.

Copies are easy to miss in a diff. TA2 Duplication Detection starts from that, so the packs hand detection to a tool. jscpd finds repeated runs of tokens across files, 50 tokens or more by default, and exits with an error when duplication passes its --threshold percentage. The TypeScript, JavaScript and Python packs run it with --threshold 0, so any clone fails, as TA2 asks for new code. The TypeScript and JavaScript command is:

npx jscpd . \
  --ignore "**/node_modules/**,**/dist/**,**/build/**,**/coverage/**,**/.nuxt/**,**/.output/**" \
  --format "typescript,tsx,javascript,jsx,vue,svelte,astro" \
  --threshold 0

In a test on 12 October 2026, jscpd 5.4.1 with --threshold 0 exited with code 1 on two files that held the same seven-line function under different names.

Python adds Pylint’s duplicate-code check, and TypeScript and JavaScript add sonarjs’s no-identical-functions. The Java and PHP packs name PMD’s CPD, with the same 50-token minimum.

Magic numbers

A magic number is a numeric literal whose meaning isn’t clear to the reader, as Wikipedia’s article on magic numbers defines it. Wikipedia’s Code smell page lists excessive literals among its examples. Fowler’s catalogue fixes one with Replace Magic Literal. His changes page says it appears only in the second edition’s web version. The first edition called it Replace Magic Number with Symbolic Constant.

In basketTotalPence >= 5000, the reader has to work out that 5000 is the basket total for free delivery, in pence. A named constant records that once. S5 Inline-First puts it as “Literals with meaning earn a const NAME”, and F3 says “Constants over magic literals”.

Viewing: TypeScript.
const FREE_DELIVERY_FROM_PENCE = 5000;

export const isDeliveryFree = (basketTotalPence: number): boolean =>
  basketTotalPence >= FREE_DELIVERY_FROM_PENCE;

From release 3.0.0 every pack has a magic-number rule, and each one looks in different places:

Language Rule What it flags What it lets through
TypeScript @typescript-eslint/no-magic-numbers, base rule off Any number except a const’s value or an object’s property -1, 0 and 1, array indexes, default values, enums, numeric literal types, readonly class properties and type indexes
JavaScript no-magic-numbers Any number except a const’s value or an object’s property -1, 0 and 1, array indexes and default values
Python Ruff PLR2004 Numbers in comparisons Strings, bytes and the numbers 0, 1 and -1
Java PMD AvoidLiteralsInIfCondition Literals in a simple if condition -1, 0 and 1 as well as null, true and false
PHP phpmnd Numbers in conditions, returns, case labels, arguments, assignments, operations and property defaults 0, 1 and -1, default parameter values and numbers in arrays or array indexes

The Python and Java rules look at fewer places than the others. PLR2004 checks comparisons and PMD checks if conditions, so a number in a calculation passes both. The Java pack names Checkstyle’s MagicNumber check to pair with PMD for the rest, set to allow -1, 0 and 1.

Code smells the packs leave to review

Most of Fowler and Beck’s list is about design. The packs have no rule aimed at these smells:

Smell What it looks like
Mysterious name A name that doesn’t say what it holds or does
Feature envy A function that spends more time with another module’s data than with its own
Shotgun surgery A change that needs small edits in many classes
Divergent change One module that changes for several unrelated reasons
Primitive obsession Plain numbers and strings standing in for things such as money or a phone number
Data clumps Values that keep travelling together
Message chains Code that asks one object for another, then asks that one for a third

Each pack checks how a name is written, such as its case, and leaves its meaning to a reviewer. max-params catches a data clump only once a function has four parameters.

Comments are on the chapter’s list too. Fowler and Beck treat a comment that explains unclear code as a cover for a smell. Every pack fails on any comment except tool directives, SPDX licence lines and generated-file banners. That removes the comment, but the smell it covered stays until someone refactors. F5 Self-Documenting Code asks for the refactoring that makes the comment unnecessary.

A reviewer has to look for all of these. The AI code review checklist covers what a reviewer, person or agent, should look for. Refactoring with a coding agent takes the fixes one small step at a time behind tests.

How to fix a code smell the linter finds

  1. Find the smell behind the rule named in the error, using the table of lint rules above.
  2. Check that tests cover the behaviour you’re about to change, and add them if they don’t.
  3. Apply the matching refactoring from the catalogue, one at a time.
  4. Run the tests and the linter again.
  5. Leave the limit and the allowed list as they are, and don’t add a comment that disables the rule. TA1 Linter as Law calls the disable comment the bug.