Finding Reference
This page is generated by docs/generate.py from src/noze/glossary/docs.rs.
API Clarity and Constants
Boolean Blindness (boolean_blindness)
What it is
Bare booleans whose meaning is invisible at the call site (f(true, false)) — use a closed type (enum or literal union) or named options so calls read clearly.
Why it's bad
Call sites stop reading like code and start reading like truth tables.
Example
Problem
def publish_invoice(
invoice_id: str,
email_customer: bool,
archive_pdf: bool,
notify_owner: bool,
) -> None:
invoice = repo.load(invoice_id)
if email_customer:
mailer.send(invoice)
else:
review_queue.add(invoice)
if archive_pdf:
archive.store(render_pdf(invoice))
if notify_owner:
notifier.ping(invoice.owner)
Proposed fix
Promote each boolean decision into a named strategy so callers choose behavior explicitly.
from typing import Protocol
class InvoiceDelivery(Protocol):
def deliver(self, invoice: Invoice) -> None: ...
class InvoiceFollowUp(Protocol):
def run(self, invoice: Invoice) -> None: ...
class EmailDelivery:
def deliver(self, invoice: Invoice) -> None:
mailer.send(invoice)
class ReviewQueueDelivery:
def deliver(self, invoice: Invoice) -> None:
review_queue.add(invoice)
class PdfArchival:
def run(self, invoice: Invoice) -> None:
archive.store(render_pdf(invoice))
class OwnerNotification:
def run(self, invoice: Invoice) -> None:
notifier.ping(invoice.owner)
def publish_invoice(
invoice_id: str,
delivery: InvoiceDelivery,
follow_ups: list[InvoiceFollowUp],
) -> None:
invoice = repo.load(invoice_id)
delivery.deliver(invoice)
for follow_up in follow_ups:
follow_up.run(invoice)
Problem
function publishInvoice(
invoiceId: string,
emailCustomer: boolean,
archivePdf: boolean,
notifyOwner: boolean,
): void {
const invoice = repo.load(invoiceId);
if (emailCustomer) {
mailer.send(invoice);
} else {
reviewQueue.add(invoice);
}
if (archivePdf) {
archive.store(renderPdf(invoice));
}
if (notifyOwner) {
notifier.ping(invoice.owner);
}
}
Proposed fix
Promote each boolean decision into a named strategy so callers choose behavior explicitly.
interface InvoiceDelivery {
deliver(invoice: Invoice): void;
}
interface InvoiceFollowUp {
run(invoice: Invoice): void;
}
class EmailDelivery implements InvoiceDelivery {
deliver(invoice: Invoice): void {
mailer.send(invoice);
}
}
class ReviewQueueDelivery implements InvoiceDelivery {
deliver(invoice: Invoice): void {
reviewQueue.add(invoice);
}
}
class PdfArchival implements InvoiceFollowUp {
run(invoice: Invoice): void {
archive.store(renderPdf(invoice));
}
}
class OwnerNotification implements InvoiceFollowUp {
run(invoice: Invoice): void {
notifier.ping(invoice.owner);
}
}
function publishInvoice(
invoiceId: string,
delivery: InvoiceDelivery,
followUps: InvoiceFollowUp[],
): void {
const invoice = repo.load(invoiceId);
delivery.deliver(invoice);
for (const followUp of followUps) {
followUp.run(invoice);
}
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.boolean_blindness]
enabled = true
action = "warning"
max_bool_params = 2 # allowed boolean parameters before flagging
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
Literal Membership (literal_membership)
What it is
Branching on membership in a literal string list, or on three or more ===/== string comparisons of one value joined by ||/or — stringly-typed categories; use a closed type (enum or literal union). Punctuation sets and markup tag names are outside vocabularies and stay quiet.
Why it's bad
Hard-coded string sets become a hidden enum that tools cannot help with.
Example
Problem
def is_allowed(status: str) -> bool:
return status in ["draft", "paid", "void"]
Proposed fix
Use an Enum or named constant set with a typed boundary.
from enum import Enum
class InvoiceStatus(Enum):
DRAFT = "draft"
PAID = "paid"
VOID = "void"
def is_allowed(status: InvoiceStatus) -> bool:
return status in {
InvoiceStatus.DRAFT,
InvoiceStatus.PAID,
InvoiceStatus.VOID,
}
Problem
function isAllowed(status: string): boolean {
return ["draft", "paid", "void"].includes(status);
}
Proposed fix
Use a string-literal union type or a named as const set shared with its type.
enum InvoiceStatus {
Draft = "draft",
Paid = "paid",
Void = "void",
}
function isAllowed(status: InvoiceStatus): boolean {
return status === InvoiceStatus.Draft ||
status === InvoiceStatus.Paid ||
status === InvoiceStatus.Void;
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.literal_membership]
enabled = true
action = "warning"
skip_external_vocab = true # ignore extension, MIME, path, and error-code lists
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
Magic String Default (magic_string_default)
What it is
A fallback empty, one-character, or placeholder string (?? "", or "n/a") is stored, compared, or returned in place of a missing value — the contract is hiding in a sentinel; prefer a nullable/optional string or a dedicated sum type.
Why it's bad
A sentinel string hides the real optionality of the value.
Example
Flags empty, one-character, or sentinel-word fallbacks that are stored, sent, or compared.
Problem
def save_owner(owner: Owner) -> None:
store.save(label=owner.label or "")
Proposed fix
Reject a missing required string explicitly instead of hiding it behind a sentinel fallback.
def display_name(name: str | None) -> str:
if name is None:
raise ValueError("display name is required")
return name
Also flags empty or one-character fallbacks that are returned or kept in a variable; skips rendered text and string operations, including coercions such as Number(..) and lookups such as map.get(..).
Problem
def display_name(name: str | None) -> str:
return name or ""
Proposed fix
Reject a missing required string explicitly instead of hiding it behind a sentinel fallback.
def display_name(name: str | None) -> str:
if name is None:
raise ValueError("display name is required")
return name
Flags every empty, one-character, or sentinel-word fallback wherever it flows.
Problem
def greeting(name: str | None) -> str:
return f"Hello {name or ''}"
Proposed fix
Reject a missing required string explicitly instead of hiding it behind a sentinel fallback.
def display_name(name: str | None) -> str:
if name is None:
raise ValueError("display name is required")
return name
Flags empty, one-character, or sentinel-word fallbacks that are stored, sent, or compared.
Problem
async function saveOwner(owner: Owner): Promise<void> {
await store.save({ label: owner.label ?? "n/a" });
}
Proposed fix
Throw on a missing required string, or model optionality explicitly when absence is valid.
function displayName(name?: string): string {
if (name === undefined) {
throw new Error("display name is required");
}
return name;
}
Also flags empty or one-character fallbacks that are returned or kept in a variable; skips rendered text and string operations, including coercions such as Number(..) and lookups such as map.get(..).
Problem
function displayName(name?: string): string {
return name || "?";
}
Proposed fix
Throw on a missing required string, or model optionality explicitly when absence is valid.
function displayName(name?: string): string {
if (name === undefined) {
throw new Error("display name is required");
}
return name;
}
Flags every empty, one-character, or sentinel-word fallback wherever it flows.
Problem
function greeting(name?: string): string {
return `Hello ${name ?? ""}`;
}
Proposed fix
Throw on a missing required string, or model optionality explicitly when absence is valid.
function displayName(name?: string): string {
if (name === undefined) {
throw new Error("display name is required");
}
return name;
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.magic_string_default]
enabled = true
action = "warning"
strictness = "low"
Flags empty, one-character, or sentinel-word fallbacks that are stored, sent, or compared.
[smells.<lang>.rules.magic_string_default]
enabled = true
action = "warning"
strictness = "medium"
Also flags empty or one-character fallbacks that are returned or kept in a variable; skips rendered text and string operations, including coercions such as Number(..) and lookups such as map.get(..).
[smells.<lang>.rules.magic_string_default]
enabled = true
action = "warning"
strictness = "high"
Flags every empty, one-character, or sentinel-word fallback wherever it flows.
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
Magic Numbers (magic_numbers)
What it is
Unexplained numeric literals in logic — name them as constants so their intent is clear.
Why it's bad
Numbers without names are hard to audit and easy to copy blindly.
Example
Problem
def is_retryable(attempts: int) -> bool:
return attempts < 7
Proposed fix
Extract a named constant near the policy it represents.
MAX_RETRIES = 7
def is_retryable(attempts: int) -> bool:
return attempts < MAX_RETRIES
Problem
function isRetryable(attempts: number): boolean {
return attempts < 7;
}
Proposed fix
Use a named const or configuration value for the policy number.
const MAX_RETRIES = 7;
function isRetryable(attempts: number): boolean {
return attempts < MAX_RETRIES;
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.magic_numbers]
enabled = true
action = "warning"
ignore = [] # numeric literals never reported
ignore_array_indexes = true # skip literal array indexes such as x[3]
ignore_default_values = true # skip literals in parameter default values
allow_http_status = true # skip 100-599 compared or assigned to a status field
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | No |
| JS / TS | No |
| Rust | No |
External linter coverage
- Ruff:
PLR2004 - ESLint:
no-magic-numbers
Complexity and Control Flow
Deep Nesting (deep_nesting)
What it is
Control flow nests many levels deep, hard to follow — flatten with early returns or extracted helpers.
Why it's bad
The control flow becomes hard to scan and easy to misread in review.
Example
Problem
def approve(order: Order) -> bool:
if order.is_paid:
if not order.is_flagged:
if order.customer.is_active:
if order.customer.is_verified:
if order.total < order.customer.limit:
return True
return False
Proposed fix
Use guard clauses, continue early, or extract a helper for the nested branch.
def approve(order: Order) -> bool:
if not order.is_paid or order.is_flagged:
return False
if not order.customer.is_active:
return False
if not order.customer.is_verified:
return False
return order.total < order.customer.limit
Problem
function approve(order: Order): boolean {
if (order.isPaid) {
if (!order.isFlagged) {
if (order.customer.isActive) {
if (order.customer.isVerified) {
if (order.total < order.customer.limit) {
return true;
}
}
}
}
}
return false;
}
Proposed fix
Flatten with early returns/continues or pull nested checks into named helpers.
function approve(order: Order): boolean {
if (!order.isPaid || order.isFlagged) {
return false;
}
if (!order.customer.isActive) {
return false;
}
if (!order.customer.isVerified) {
return false;
}
return order.total < order.customer.limit;
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.deep_nesting]
enabled = true
action = "warning"
max_nesting = 4 # allowed nested block depth
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | No |
| Rust | Yes |
External linter coverage
- Ruff:
PLR1702 - ESLint:
max-depth
High Cognitive Complexity (high_cognitive_complexity)
What it is
Hard for a human to follow — nested branches and loops weighted by depth; simplify or break it up.
Why it's bad
The reader has to simulate too many branches and nesting levels at once.
Example
Problem
def score(order: Order) -> int:
total = 0
if order.paid:
if order.customer.active:
if not order.customer.suspended:
if order.customer.verified:
if order.total >= 500:
total += 25
elif order.total >= 100:
total += 10
else:
total += 1
return total
Proposed fix
Name the decision steps, flatten branches, and extract cohesive helpers.
NO_SCORE = 0
ENTERPRISE_ORDER_THRESHOLD = 500
LARGE_ORDER_THRESHOLD = 100
ENTERPRISE_ORDER_SCORE = 25
LARGE_ORDER_SCORE = 10
STANDARD_ORDER_SCORE = 1
def score(order: Order) -> int:
if not can_score(order):
return NO_SCORE
return score_total(order.total)
def can_score(order: Order) -> bool:
if not order.paid:
return False
if not order.customer.active:
return False
if order.customer.suspended:
return False
return order.customer.verified
def score_total(total: int) -> int:
if total >= ENTERPRISE_ORDER_THRESHOLD:
return ENTERPRISE_ORDER_SCORE
if total >= LARGE_ORDER_THRESHOLD:
return LARGE_ORDER_SCORE
return STANDARD_ORDER_SCORE
Problem
function score(order: Order): number {
let total = 0;
if (order.paid) {
if (order.customer.active) {
if (!order.customer.suspended) {
if (order.customer.verified) {
if (order.total >= 500) {
total += 25;
} else if (order.total >= 100) {
total += 10;
} else {
total += 1;
}
}
}
}
}
return total;
}
Proposed fix
Prefer guard clauses and small predicate functions over nested branches.
const NO_SCORE = 0;
const ENTERPRISE_ORDER_THRESHOLD = 500;
const LARGE_ORDER_THRESHOLD = 100;
const ENTERPRISE_ORDER_SCORE = 25;
const LARGE_ORDER_SCORE = 10;
const STANDARD_ORDER_SCORE = 1;
function score(order: Order): number {
if (!canScore(order)) {
return NO_SCORE;
}
return scoreTotal(order.total);
}
function canScore(order: Order): boolean {
if (!order.paid) {
return false;
}
if (!order.customer.active) {
return false;
}
if (order.customer.suspended) {
return false;
}
return order.customer.verified;
}
function scoreTotal(total: number): number {
if (total >= ENTERPRISE_ORDER_THRESHOLD) {
return ENTERPRISE_ORDER_SCORE;
}
if (total >= LARGE_ORDER_THRESHOLD) {
return LARGE_ORDER_SCORE;
}
return STANDARD_ORDER_SCORE;
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.high_cognitive_complexity]
enabled = true
action = "warning"
max_cognitive = 15 # allowed cognitive complexity score
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | No |
| Rust | Yes |
External linter coverage
- ESLint:
sonarjs/cognitive-complexity
High Cyclomatic Complexity (high_complexity)
What it is
Many independent paths through the function, so it's hard to test fully — decompose it.
Why it's bad
The function accumulates too many distinct paths to reason about confidently.
Example
Problem
def choose_refund(order: Order) -> RefundDecision:
if order.cancelled:
return deny_refund(order, "cancelled")
if order.fraud_review:
return manual_review(order)
if order.days_since_purchase > 30:
return deny_refund(order, "expired")
if order.damaged:
return full_refund(order)
if order.missing_items:
return partial_refund(order)
if order.vip_customer:
return goodwill_credit(order)
return deny_refund(order, "not_eligible")
Proposed fix
Use a dispatch table or split the branches into named operations.
from collections.abc import Callable
RefundHandler = Callable[[Order], RefundDecision]
REFUND_POLICY: dict[str, RefundHandler] = {
"cancelled": deny_cancelled,
"fraud_review": manual_review,
"expired": deny_expired,
"damaged": full_refund,
"missing_items": partial_refund,
"vip_customer": goodwill_credit,
}
def choose_refund(order: Order) -> RefundDecision:
handler = REFUND_POLICY.get(order.refund_reason(), deny_not_eligible)
return handler(order)
Problem
function chooseRefund(order: Order): RefundDecision {
if (order.cancelled) {
return denyRefund(order, "cancelled");
}
if (order.fraudReview) {
return manualReview(order);
}
if (order.daysSincePurchase > 30) {
return denyRefund(order, "expired");
}
if (order.damaged) {
return fullRefund(order);
}
if (order.missingItems) {
return partialRefund(order);
}
if (order.vipCustomer) {
return goodwillCredit(order);
}
return denyRefund(order, "not_eligible");
}
Proposed fix
Use a map, strategy object, or smaller functions for independent paths.
type RefundHandler = (order: Order) => RefundDecision;
const refundPolicy: Record<string, RefundHandler> = {
cancelled: denyCancelled,
fraud_review: manualReview,
expired: denyExpired,
damaged: fullRefund,
missing_items: partialRefund,
vip_customer: goodwillCredit,
};
function chooseRefund(order: Order): RefundDecision {
const handler = refundPolicy[order.refundReason()] ?? denyNotEligible;
return handler(order);
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.high_complexity]
enabled = true
action = "warning"
max_cyclomatic = 10 # allowed independent control-flow paths
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | No |
| JS / TS | No |
| Rust | No |
External linter coverage
- Ruff:
PLR0912 - ESLint:
complexity
Too Many Returns (too_many_returns)
What it is
Many exit points make the function's flow hard to follow — consolidate, or it's doing too much.
Why it's bad
Many exit points make the function harder to follow and test.
Example
Problem
def status_for(order: Order) -> str:
if order.cancelled:
return "cancelled"
elif order.refunded:
return "refunded"
elif order.failed:
return "failed"
elif order.paid:
return "paid"
return "open"
Proposed fix
Group related guards or extract decision helpers.
def status_for(order: Order) -> str:
rules = (
(order.cancelled, "cancelled"),
(order.refunded, "refunded"),
(order.failed, "failed"),
(order.paid, "paid"),
)
for applies, status in rules:
if applies:
return status
return "open"
Problem
function statusFor(order: Order): string {
if (order.cancelled) {
return "cancelled";
} else if (order.refunded) {
return "refunded";
} else if (order.failed) {
return "failed";
} else if (order.paid) {
return "paid";
}
return "open";
}
Proposed fix
Keep meaningful guard clauses, then extract noisy branches into predicates.
const ORDER_STATUS_RULES: Array<[keyof Order, string]> = [
["cancelled", "cancelled"],
["refunded", "refunded"],
["failed", "failed"],
["paid", "paid"],
];
function statusFor(order: Order): string {
for (const [field, status] of ORDER_STATUS_RULES) {
if (order[field]) {
return status;
}
}
return "open";
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.too_many_returns]
enabled = true
action = "warning"
max_returns = 4 # allowed return statements before flagging
count_guard_clauses = false # also count top-level guard-clause returns
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | No |
| JS / TS | No |
| Rust | No |
External linter coverage
- Ruff:
PLR0911
Nested Ternary (nested_ternary)
What it is
A conditional expression is a branch or condition of another conditional expression, forcing readers to match several branches mentally — extract the result into a named function or flatten it into if statements with early returns.
Why it's bad
Nested conditional expressions force readers to mentally match several conditions with their results, making agent-generated logic difficult to review and change.
Example
Flags direct chains, where one conditional is a branch or condition of another.
Problem
def delivery_status(order: Order) -> DeliveryStatus:
return (
DeliveryStatus.READY
if order.is_paid
else DeliveryStatus.RETRY
if order.payment_retry_allowed
else DeliveryStatus.BLOCKED
)
Proposed fix
Extract the decision into a named helper, or replace the expression with ordered if statements and early returns.
def delivery_status(order) -> DeliveryStatus:
if order.is_paid:
return DeliveryStatus.READY
if order.payment_retry_allowed:
return DeliveryStatus.RETRY
return DeliveryStatus.BLOCKED
Also flags conditionals nested inside a call argument or template of another; JSX elements and object literals end the nesting.
Problem
def delivery_label(order: Order) -> str:
return (
format_label("gift" if order.is_gift else "standard")
if order.is_paid
else "pending"
)
Proposed fix
Extract the decision into a named helper, or replace the expression with ordered if statements and early returns.
def delivery_status(order) -> DeliveryStatus:
if order.is_paid:
return DeliveryStatus.READY
if order.payment_retry_allowed:
return DeliveryStatus.RETRY
return DeliveryStatus.BLOCKED
Flags direct chains, where one conditional is a branch or condition of another.
Problem
export function deliveryStatus(order: Order): DeliveryStatus {
return order.isPaid
? DeliveryStatus.Ready
: order.paymentRetryAllowed
? DeliveryStatus.Retry
: DeliveryStatus.Blocked;
}
Proposed fix
Move result selection into a named function and use explicit if branches with early returns.
function deliveryStatus(order: Order): DeliveryStatus {
if (order.isPaid) {
return DeliveryStatus.Ready;
}
if (order.paymentRetryAllowed) {
return DeliveryStatus.Retry;
}
return DeliveryStatus.Blocked;
}
Also flags conditionals nested inside a call argument or template of another; JSX elements and object literals end the nesting.
Problem
export function deliveryLabel(order: Order): string {
return order.isPaid ? formatLabel(order.isGift ? "gift" : "standard") : "pending";
}
Proposed fix
Move result selection into a named function and use explicit if branches with early returns.
function deliveryStatus(order: Order): DeliveryStatus {
if (order.isPaid) {
return DeliveryStatus.Ready;
}
if (order.paymentRetryAllowed) {
return DeliveryStatus.Retry;
}
return DeliveryStatus.Blocked;
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.nested_ternary]
enabled = true
action = "warning"
strictness = "medium"
Flags direct chains, where one conditional is a branch or condition of another.
[smells.<lang>.rules.nested_ternary]
enabled = true
action = "warning"
strictness = "high"
Also flags conditionals nested inside a call argument or template of another; JSX elements and object literals end the nesting.
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
External linter coverage
- ESLint:
no-nested-ternary
Unnecessary Nested If (unnecessary_nested_if)
What it is
An if whose only body is another if, with no else path — combine the conditions with and/&& to flatten the control flow.
Why it's bad
The code says 'if' twice when the condition really belongs on one line.
Example
Problem
def can_ship(order: Order) -> bool:
if order.paid:
if order.address.is_valid:
return True
return False
Proposed fix
Combine the conditions or use a guard clause.
def can_ship(order: Order) -> bool:
if not order.paid:
return False
return order.address.is_valid
Problem
function canShip(order: Order): boolean {
if (order.paid) {
if (order.address.isValid) {
return true;
}
}
return false;
}
Proposed fix
Collapse nested conditions with && or extract a predicate.
function canShip(order: Order): boolean {
if (!order.paid) {
return false;
}
return order.address.isValid;
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.unnecessary_nested_if]
enabled = true
action = "warning"
# This detector has no extra threshold knobs.
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
External linter coverage
- ESLint:
sonarjs/no-collapsible-if
Redundant Validation (redundant_validation)
What it is
The same condition is checked repeatedly in one function — establish the invariant once and simplify the later path.
Why it's bad
Repeated guards obscure which invariants have already been established.
Example
Problem
def publish(message):
if message is None:
return
prepare(message)
if message is None:
return
send(message)
Proposed fix
Validate at the boundary or first use, then rely on the established invariant.
def publish(message):
if message is None:
return
prepare(message)
send(message)
Problem
function publish(message: Message | undefined) {
if (message === undefined) return;
prepare(message);
if (message === undefined) return;
send(message);
}
Proposed fix
Validate at the boundary or first use, then rely on the established invariant.
function publish(message: Message | undefined) {
if (message === undefined) return;
prepare(message);
send(message);
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.redundant_validation]
enabled = true
action = "warning"
# This detector has no extra threshold knobs.
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | No |
| JS / TS | No |
| Rust | No |
Size and Responsibility
God Module (god_module)
What it is
A sizeable module with its own functions or classes that many modules depend on and that itself depends on many internal modules at runtime (high centrality) — a coupling and change-risk hotspot; split its responsibilities. A composition root of imports and one data object is not one.
Why it's bad
A single module becomes a hot spot with too many reasons to change.
Example
The example is kept short, so it fires with these settings:
# The example stays small; at defaults a god module needs a fan of 25, 150 lines
# and 12 functions or classes, and with four callers shotgun_surgery_hazard wins.
[smells]
god_module_fan = 4
god_module_min_fan_in = 4
god_module_min_fan_out = 4
god_module_min_lines = 0
god_module_min_units = 0
shotgun_blast_threshold = 5
Problem
from god_module.dep_a import run as load_user
from god_module.dep_b import run as charge_user
from god_module.dep_c import run as send_receipt
from god_module.dep_d import run as archive_receipt
def process_checkout() -> int:
return load_user() + charge_user() + send_receipt() + archive_receipt()
Proposed fix
Split by dependency direction and cohesive responsibility.
def load_user(user_id: str) -> User:
return users.find(user_id)
def charge_user(user: User) -> Receipt:
return billing.charge(user)
def send_receipt(user: User, receipt: Receipt) -> None:
mailer.send(user.email, receipt)
def archive_receipt(receipt: Receipt) -> None:
archive.save(receipt)
Problem
import { run as loadUser } from "./dep_a";
import { run as chargeUser } from "./dep_b";
import { run as sendReceipt } from "./dep_c";
import { run as archiveReceipt } from "./dep_d";
export function processCheckout(): number {
return loadUser() + chargeUser() + sendReceipt() + archiveReceipt();
}
Proposed fix
Extract smaller modules and route callers through a narrow public API.
function loadUser(userId: string): User {
return users.find(userId);
}
function chargeUser(user: User): Receipt {
return billing.charge(user);
}
function sendReceipt(user: User, receipt: Receipt): void {
mailer.send(user.email, receipt);
}
function archiveReceipt(receipt: Receipt): void {
archive.save(receipt);
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.god_module]
enabled = true
action = "warning"
min_fan = 25 # combined incoming/outgoing fan before god-module risk
min_fan_in = 5 # importing modules required for a god module
min_fan_out = 10 # internal runtime imports required for a god module
min_lines = 150 # module lines required for a god module (or min_units)
min_units = 12 # top-level functions and classes required (or min_lines)
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
Heavy Nested Function (heavy_nested_function)
What it is
An inner/nested function that grew large and logic-heavy — promote it to a top-level, testable function.
Why it's bad
Nested helpers hide important behavior and make tests awkward.
Example
The example is kept short, so it fires with these settings:
# The example stays short; at defaults the nested function needs more than 15 lines.
[smells]
max_nested_function_lines = 4
Problem
def import_customers(rows: list[dict[str, str]]) -> list[Customer]:
def normalize(row: dict[str, str]) -> Customer:
email = row["email"].strip().lower()
status = row["status"].strip()
plan = row["plan"].strip()
region = row["region"].strip()
return Customer(email, status, plan, region)
return [normalize(row) for row in rows]
Proposed fix
Promote the helper to a top-level private function with direct tests.
def normalize_customer(row: Row) -> Customer:
email = row["email"].strip().lower()
status = row["status"].strip()
plan = row["plan"].strip()
region = row["region"].strip()
return Customer(email, status, plan, region)
def import_customers(rows: Rows) -> Customers:
return [normalize_customer(row) for row in rows]
Problem
function importCustomers(rows: Row[]): Customer[] {
function normalize(row: Row): Customer {
const email = row.email.trim().toLowerCase();
const status = row.status.trim();
const plan = row.plan.trim();
const region = row.region.trim();
return new Customer(email, status, plan, region);
}
return rows.map(normalize);
}
Proposed fix
Move the nested function to module scope or a small collaborator.
function normalizeCustomer(row: Row): Customer {
const email = row.email.trim().toLowerCase();
const status = row.status.trim();
const plan = row.plan.trim();
const region = row.region.trim();
return new Customer(email, status, plan, region);
}
function importCustomers(rows: Row[]): Customer[] {
return rows.map(normalizeCustomer);
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.heavy_nested_function]
enabled = true
action = "warning"
max_lines = 15 # allowed function or nested-function length
skip_capturing = false # drop nested functions that read parent locals
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
Large Class (large_class)
What it is
A class with too many methods/responsibilities — split it into focused classes. Related code-smell reference: Refactoring.Guru: Large Class.
Why it's bad
The class stops having a clear job and becomes a grab bag.
Example
The example is kept short, so it fires with these settings:
# The example stays short; at defaults a class needs more than 20 methods.
[smells]
max_class_methods = 3
Problem
class ReportManager:
def build(self) -> Report:
return build_report()
def send(self) -> None:
mailer.send()
def archive(self) -> None:
archive.save()
def bill(self) -> Receipt:
return billing.charge()
Proposed fix
Extract cohesive collaborators around loading, rendering, delivery, etc.
class ReportBuilder:
def build(self) -> Report:
return build_report()
class ReportMailer:
def send(self, report: Report) -> None:
mailer.send(report)
class ReportArchive:
def save(self, report: Report) -> None:
archive.save(report)
class ReportBilling:
def charge(self, report: Report) -> Receipt:
return billing.charge(report)
Problem
class ReportManager {
build(): Report {
return buildReport();
}
send(): void {
mailer.send();
}
archive(): void {
archive.save();
}
bill(): Receipt {
return billing.charge();
}
}
Proposed fix
Split the class by capability and keep a thin orchestration surface.
class ReportBuilder {
build(): Report {
return buildReport();
}
}
class ReportMailer {
send(report: Report): void {
mailer.send(report);
}
}
class ReportArchive {
save(report: Report): void {
archive.save(report);
}
}
class ReportBilling {
charge(report: Report): Receipt {
return billing.charge(report);
}
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.large_class]
enabled = true
action = "warning"
max_methods = 20 # allowed methods before a class is large
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | No |
| JS / TS | Yes |
| Rust | No |
Long Function (long_function)
What it is
Too many lines to grasp at once — extract cohesive pieces. Related code-smell reference: Refactoring.Guru: Long Method.
Why it's bad
The function becomes a scroll instead of a unit you can hold in your head.
Example
Problem
def import_orders(rows: Rows) -> Orders:
parsed = [parse_order(row) for row in rows]
valid = [order for order in parsed if order.is_valid]
enriched = [enrich(order) for order in valid]
totals = [calculate_total(order) for order in enriched]
discounts = [calculate_discount(order) for order in enriched]
taxes = [calculate_tax(order) for order in enriched]
save_orders(enriched)
notify_import_complete(enriched)
archive_totals(totals)
archive_discounts(discounts)
archive_taxes(taxes)
return enriched
Proposed fix
Extract named chunks that each complete one step of the workflow.
def import_orders(rows: Rows) -> Orders:
orders = _valid_orders(rows)
enriched = [enrich(order) for order in orders]
_persist_import(enriched)
return enriched
def _valid_orders(rows: Rows) -> Orders:
parsed = [parse_order(row) for row in rows]
return [order for order in parsed if order.is_valid]
def _persist_import(orders: Orders) -> None:
save_orders(orders)
notify_import_complete(orders)
archive_totals([calculate_total(order) for order in orders])
Problem
function importOrders(rows: Rows): Orders {
const parsed = rows.map(parseOrder);
const valid = parsed.filter((order) => order.isValid);
const enriched = valid.map(enrich);
const totals = enriched.map(calculateTotal);
const discounts = enriched.map(calculateDiscount);
const taxes = enriched.map(calculateTax);
saveOrders(enriched);
notifyImportComplete(enriched);
archiveTotals(totals);
archiveDiscounts(discounts);
archiveTaxes(taxes);
return enriched;
}
Proposed fix
Extract cohesive helper functions and keep the orchestration readable.
function importOrders(rows: Rows): Orders {
const orders = validOrders(rows);
const enriched = orders.map(enrich);
persistImport(enriched);
return enriched;
}
function validOrders(rows: Rows): Orders {
const parsed = rows.map(parseOrder);
return parsed.filter((order) => order.isValid);
}
function persistImport(orders: Orders): void {
saveOrders(orders);
notifyImportComplete(orders);
archiveTotals(orders.map(calculateTotal));
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.long_function]
enabled = true
action = "warning"
max_lines = 50 # allowed function or nested-function length
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | No |
| JS / TS | No |
| Rust | No |
External linter coverage
- Ruff:
PLR0915 - ESLint:
max-lines-per-function
Long Parameter List (long_parameter_list)
What it is
Too many parameters — group related ones into an object, or the function is doing too much. Related code-smell reference: Refactoring.Guru: Long Parameter List.
Why it's bad
The call contract becomes noisy and easy to pass in the wrong order.
Example
Problem
def search(term: str, page: int, per_page: int, sort: str) -> SearchResults:
return index.find(term, page, per_page, sort)
Proposed fix
Group related parameters into a dataclass or keyword-only options object.
from dataclasses import dataclass
@dataclass(frozen=True)
class SearchQuery:
term: str
page: int
per_page: int
sort: str
def search(query: SearchQuery) -> SearchResults:
return index.find(query)
Problem
function search(term: string, page: number, perPage: number, sort: string): SearchResults {
return index.find(term, page, perPage, sort);
}
Proposed fix
Use an options object or domain type instead of positional arguments.
interface SearchQuery {
term: string;
page: number;
perPage: number;
sort: string;
}
function search(query: SearchQuery): SearchResults {
return index.find(query);
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.long_parameter_list]
enabled = true
action = "warning"
max_params = 5 # allowed parameters before flagging
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | No |
| JS / TS | No |
| Rust | No |
External linter coverage
- Ruff:
PLR0913 - ESLint:
max-params
Narrating Code (narrating_code)
What it is
A function is packed with step-by-step narration or commented-out code — prefer clearer names or extracted helpers, delete dead code, and keep comments for why.
Why it's bad
The prose becomes a second implementation that readers must keep in sync with the code.
Example
Problem
def activate_plan(account: Account) -> None:
# Load the account owner.
owner = account.owner
# Load the billing profile.
billing = owner.billing_profile
# Check whether billing is enabled.
if not billing.enabled:
return
# Load the selected plan.
plan = billing.selected_plan
# Activate the selected plan.
account.activate(plan)
# Notify the owner.
notify_owner(owner, plan)
Proposed fix
Rename values and extract helper functions so the code says what the comments were saying.
def activate_plan(account: Account) -> None:
owner = account.owner
billing = owner.billing_profile
if not billing.enabled:
return
_activate_selected_plan(account, owner, billing)
def _activate_selected_plan(
account: Account,
owner: Owner,
billing: BillingProfile,
) -> None:
plan = billing.selected_plan
account.activate(plan)
notify_owner(owner, plan)
Problem
function activatePlan(account: Account): void {
// Load the account owner.
const owner = account.owner;
// Load the billing profile.
const billing = owner.billingProfile;
// Check whether billing is enabled.
if (!billing.enabled) {
return;
}
// Load the selected plan.
const plan = billing.selectedPlan;
// Activate the selected plan.
account.activate(plan);
// Notify the owner.
notifyOwner(owner, plan);
}
Proposed fix
Extract named predicates/helpers and keep comments for constraints or rationale.
function activatePlan(account: Account): void {
const owner = account.owner;
const billing = owner.billingProfile;
if (!billing.enabled) {
return;
}
activateSelectedPlan(account, owner, billing);
}
function activateSelectedPlan(
account: Account,
owner: Owner,
billing: BillingProfile,
): void {
const plan = billing.selectedPlan;
account.activate(plan);
notifyOwner(owner, plan);
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.narrating_code]
enabled = true
action = "warning"
min_comment_lines = 5 # detector-specific threshold
max_comment_ratio_percent = 30 # detector-specific threshold
min_function_lines = 12 # shorter functions are flagged only for commented-out code
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
Coupling and Cohesion
Data Clump (data_clump)
What it is
The same group of values is passed together through many functions — bundle them into one object or typed structure. Related code-smell reference: Refactoring.Guru: Data Clumps.
Why it's bad
The same bundle has to be kept in sync everywhere it travels.
Example
Problem
def format_label(street: str, city: str, zip_code: str, country: str) -> str:
return f"{street}, {city} {zip_code}, {country}"
def shipping_zone(street: str, city: str, zip_code: str, country: str) -> str:
return zones.lookup(street, city, zip_code, country)
def is_deliverable(street: str, city: str, zip_code: str, country: str) -> bool:
return carriers.covers(street, city, zip_code, country)
Proposed fix
Introduce a dataclass, TypedDict, or domain object for the repeated fields.
from dataclasses import dataclass
@dataclass(frozen=True)
class Address:
street: str
city: str
zip_code: str
country: str
def format_label(address: Address) -> str:
return f"{address.street}, {address.city} {address.zip_code}, {address.country}"
def shipping_zone(address: Address) -> str:
return zones.lookup(address)
def is_deliverable(address: Address) -> bool:
return carriers.covers(address)
Problem
function formatLabel(street: string, city: string, zipCode: string, country: string): string {
return `${street}, ${city} ${zipCode}, ${country}`;
}
function shippingZone(street: string, city: string, zipCode: string, country: string): string {
return zones.lookup(street, city, zipCode, country);
}
function isDeliverable(street: string, city: string, zipCode: string, country: string): boolean {
return carriers.covers(street, city, zipCode, country);
}
Proposed fix
Introduce an interface or value object and pass that object through the API.
interface Address {
street: string;
city: string;
zipCode: string;
country: string;
}
function formatLabel(address: Address): string {
return `${address.street}, ${address.city} ${address.zipCode}, ${address.country}`;
}
function shippingZone(address: Address): string {
return zones.lookup(address);
}
function isDeliverable(address: Address): boolean {
return carriers.covers(address);
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.data_clump]
enabled = true
action = "warning"
min_fields = 4 # shared fields required for a data clump
min_occurrences = 3 # functions sharing the clump before flagging
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
Divergent Change (divergent_change)
What it is
One module gets edited for many unrelated reasons — it has too many responsibilities; split it along those axes. Related code-smell reference: Refactoring.Guru: Divergent Change.
Why it's bad
One file starts changing for unrelated reasons, so fixes get tangled.
Example
Problem
class InvoiceReport:
def render_header(self) -> str:
return self.title.upper()
def calculate_total(self) -> float:
return self.subtotal + self.tax
def save_archive(self) -> None:
archive.write(self.path)
Proposed fix
Split the module by reason-to-change: presentation, pricing, persistence, etc.
class InvoiceRenderer:
def render_header(self, invoice: Invoice) -> str:
return invoice.title.upper()
class InvoiceTotals:
def calculate_total(self, invoice: Invoice) -> float:
return invoice.subtotal + invoice.tax
class InvoiceArchive:
def save(self, invoice: Invoice) -> None:
archive.write(invoice.path)
Problem
class InvoiceReport {
renderHeader(): string {
return this.title.toUpperCase();
}
calculateTotal(): number {
return this.subtotal + this.tax;
}
saveArchive(): void {
archive.write(this.path);
}
}
Proposed fix
Move unrelated responsibilities into focused modules or services.
class InvoiceRenderer {
renderHeader(invoice: Invoice): string {
return invoice.title.toUpperCase();
}
}
class InvoiceTotals {
calculateTotal(invoice: Invoice): number {
return invoice.subtotal + invoice.tax;
}
}
class InvoiceArchive {
save(invoice: Invoice): void {
archive.write(invoice.path);
}
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.divergent_change]
enabled = true
action = "warning"
# This detector has no extra threshold knobs.
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
Divergent Abstraction (divergent_abstraction)
What it is
An abstraction has only two implementations that grow in different directions — remove the forced common type or narrow it to genuine shared behavior.
Why it's bad
A forced common type couples two implementations whose real responsibilities no longer match.
Example
Problem
from typing import Protocol
class Worker(Protocol):
def run(self): ...
class FileWorker(Worker):
def run(self): ...
def open_file(self): ...
def rotate_file(self): ...
class QueueWorker(Worker):
def run(self): ...
def reserve_job(self): ...
def acknowledge_job(self): ...
Proposed fix
Narrow the Protocol to genuine shared behavior, or use the concrete types directly.
class FileWorker:
def open_file(self): ...
def rotate_file(self): ...
class QueueWorker:
def reserve_job(self): ...
def acknowledge_job(self): ...
Problem
abstract class Worker {
abstract run(): void;
}
class FileWorker extends Worker {
run() {}
openFile() {}
rotateFile() {}
}
class QueueWorker extends Worker {
run() {}
reserveJob() {}
acknowledgeJob() {}
}
Proposed fix
Narrow the interface to genuine shared behavior, or use the concrete types directly.
class FileWorker {
openFile() {}
rotateFile() {}
}
class QueueWorker {
reserveJob() {}
acknowledgeJob() {}
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.divergent_abstraction]
enabled = true
action = "warning"
# This detector has no extra threshold knobs.
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
Feature Envy (feature_envy)
What it is
A method uses another object's data more than its own — move it onto the class that owns that data. Related code-smell reference: Refactoring.Guru: Feature Envy.
Why it's bad
Logic sits next to the wrong data, so edits keep reaching through a foreign object.
Example
Problem
class Account:
balance: int
fee_rate: int
limit: int
class BillingPolicy:
name: str
def fee_for(self, account: Account) -> tuple[str, int]:
fee = account.balance + account.fee_rate + account.limit
return self.name, fee
Proposed fix
Move the behavior onto the object that owns most of the data.
class Account:
balance: int
fee_rate: int
limit: int
def billing_fee(self) -> int:
return self.balance + self.fee_rate + self.limit
class BillingPolicy:
name: str
def fee_for(self, account: Account) -> tuple[str, int]:
return self.name, account.billing_fee()
Problem
class Account {
balance = 0;
feeRate = 0;
limit = 0;
}
class BillingPolicy {
name = "standard";
feeFor(account: Account): [string, number] {
const fee = account.balance + account.feeRate + account.limit;
return [this.name, fee];
}
}
Proposed fix
Put the calculation on the owning class/module or expose a narrow query method.
class Account {
balance = 0;
feeRate = 0;
limit = 0;
billingFee(): number {
return this.balance + this.feeRate + this.limit;
}
}
class BillingPolicy {
name = "standard";
feeFor(account: Account): [string, number] {
return [this.name, account.billingFee()];
}
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.feature_envy]
enabled = true
action = "warning"
# This detector has no extra threshold knobs.
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
Inappropriate Intimacy (inappropriate_intimacy)
What it is
Two classes each reach into the other's internals, so neither can change independently — narrow the shared surface or merge them. Related code-smell reference: Refactoring.Guru: Inappropriate Intimacy.
Why it's bad
Two classes know too much about each other's internals, so refactors ripple.
Example
Problem
class Account:
def __init__(self) -> None:
self._token = "secret"
self._salt = "pepper"
def audit_account() -> str:
account: Account = Account()
return account._token + account._salt
Proposed fix
Add a public method on the collaborator or merge the coupled objects.
class Account:
def __init__(self) -> None:
self._token = "secret"
self._salt = "pepper"
def audit_key(self) -> str:
return self._token + self._salt
def audit_account() -> str:
account = Account()
return account.audit_key()
Problem
class Account {
_token = "secret";
_salt = "pepper";
}
function auditAccount(): string {
const account: Account = new Account();
return account._token + account._salt;
}
Proposed fix
Expose a narrow method/property instead of reaching into internals.
class Account {
private token = "secret";
private salt = "pepper";
auditKey(): string {
return this.token + this.salt;
}
}
function auditAccount(): string {
const account = new Account();
return account.auditKey();
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.inappropriate_intimacy]
enabled = true
action = "warning"
# This detector has no extra threshold knobs.
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
Message Chain (message_chain)
What it is
A long a.b.c.d access chain couples the caller to a deep object graph (Law of Demeter) — ask the immediate collaborator instead. Related code-smell reference: Refactoring.Guru: Message Chains.
Why it's bad
Deep property walking couples the caller to the whole object graph.
Example
Problem
def country(order: Order) -> str:
return order.customer.address.city.country.name
Proposed fix
Ask the nearest object for the answer through a method or property.
class Order:
def shipping_country(self) -> str:
return self.customer.shipping_country()
class Customer:
def shipping_country(self) -> str:
return self.address.country_name()
class Address:
def country_name(self) -> str:
return self.city.country_name()
class City:
def country_name(self) -> str:
return self.country.name
def country(order: Order) -> str:
return order.shipping_country()
Problem
function country(order: Order): string {
return order.customer.address.city.country.name;
}
Proposed fix
Expose a query on the immediate collaborator instead of chaining internals.
class Order {
shippingCountry(): string {
return this.customer.shippingCountry();
}
}
class Customer {
shippingCountry(): string {
return this.address.countryName();
}
}
class Address {
countryName(): string {
return this.city.countryName();
}
}
class City {
countryName(): string {
return this.country.name;
}
}
function country(order: Order): string {
return order.shippingCountry();
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.message_chain]
enabled = true
action = "warning"
max_depth = 4 # allowed depth before flagging
ignore_roots = [] # chain roots never reported, e.g. a generated API client
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
Refused Bequest (refused_bequest)
What it is
A subclass inherits methods/fields it doesn't use or stubs out — the inheritance is wrong; prefer composition. Related code-smell reference: Refactoring.Guru: Refused Bequest.
Why it's bad
Inheritance promises behavior the subclass does not actually want.
Example
Problem
class Report:
def export_pdf(self) -> bytes:
return render_pdf(self)
def send_email(self) -> None:
mailer.send(self)
class ReadOnlyReport(Report):
def export_pdf(self) -> bytes:
raise NotImplementedError
def send_email(self) -> None:
raise NotImplementedError
Proposed fix
Prefer composition or split the base class into smaller capabilities.
class ReadOnlyReport:
def export_pdf(self) -> bytes:
return render_pdf(self)
Problem
class Report {
exportPdf(): Uint8Array {
return renderPdf(this);
}
sendEmail(): void {
mailer.send(this);
}
}
class ReadOnlyReport extends Report {
exportPdf(): Uint8Array {
throw new Error("not supported");
}
sendEmail(): void {
throw new Error("not supported");
}
}
Proposed fix
Use composition or narrower interfaces instead of a broad base class.
class ReadOnlyReport implements ExportableReport {
exportPdf(): Uint8Array {
return renderPdf(this);
}
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.refused_bequest]
enabled = true
action = "warning"
# This detector has no extra threshold knobs.
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
Shotgun Surgery Hazard (shotgun_surgery_hazard)
What it is
A symbol so widely depended-on that one change ripples across many modules — a blast-radius hotspot. Related code-smell reference: Refactoring.Guru: Shotgun Surgery.
Why it's bad
One edit fans out to many dependents.
Example
Problem
TAX_RATE = 0.2
def calculate(amount: float) -> float:
return amount * TAX_RATE
Proposed fix
Separate stable interfaces from volatile implementation details.
class TaxPolicy:
def __init__(self, rate: float) -> None:
self.rate = rate
def calculate(self, amount: float) -> float:
return amount * self.rate
Problem
export const TAX_RATE = 0.2;
export function calculate(amount: number): number {
return amount * TAX_RATE;
}
Proposed fix
Reduce fan-in by splitting policy from shared utility shape.
class TaxPolicy {
constructor(private readonly rate: number) {}
calculate(amount: number): number {
return amount * this.rate;
}
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.shotgun_surgery_hazard]
enabled = true
action = "warning"
min_blast = 4 # dependent modules before shotgun-surgery risk
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
Sibling Helper Redefinition (sibling_helper_redefinition)
What it is
The same small helper — same name, same parameter count, near-identical body — is written again in another module instead of imported.
Why it's bad
A fix lands in one copy and the others drift; a reviewer reading one file cannot know the helper already exists.
Example
Problem
def message_of(error):
return str(error.args[0]) if error.args else repr(error)
Proposed fix
Keep one definition in a shared module and import it where the copies were.
import logging
from example import message_of
def log_failure(failure):
logging.error(message_of(failure))
Problem
export function messageOf(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
Proposed fix
Export one definition from a shared module and import it where the copies were.
import { messageOf } from "./example";
export function logFailure(failure: unknown): void {
console.error(messageOf(failure));
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.sibling_helper_redefinition]
enabled = true
action = "warning"
min_similarity = 80 # percent of matching body lexemes required between copies
min_lexemes = 8 # lexeme codes a helper body needs before it is compared
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
Type and Data Modeling
Implicit Schema (implicit_schema)
What it is
A dict/object accessed by many string keys — an unwritten schema; model it as a typed structure.
Why it's bad
Stringly typed payloads drift silently when the shape changes.
Example
Problem
def create_user(payload: dict[str, object]) -> User:
email = payload["email"]
plan = payload["plan"]
active = payload["active"]
seats = payload["seats"]
return users.create(email, plan, active, seats)
Proposed fix
Model the payload with a dataclass, TypedDict, or Pydantic model.
from dataclasses import dataclass
@dataclass(frozen=True)
class UserPayload:
email: str
plan: str
active: bool
seats: int
def create_user(payload: UserPayload) -> User:
return users.create(payload)
Problem
function createUser(payload: Record<string, unknown>): User {
const email = payload["email"];
const plan = payload["plan"];
const active = payload["active"];
const seats = payload["seats"];
return users.create(email, plan, active, seats);
}
Proposed fix
Replace loose records with an interface or validated schema.
interface UserPayload {
email: string;
plan: string;
active: boolean;
seats: number;
}
function createUser(payload: UserPayload): User {
return users.create(payload);
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.implicit_schema]
enabled = true
action = "warning"
min_keys = 4 # string keys on one object before implicit-schema risk
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
Loose Typing (loose_typing)
What it is
A public signature or class field leans on vague types (Any/untyped/overly broad) — tighten annotations so callers and tools know the contract. Casts inside a body belong to type_escape_cast.
Why it's bad
Weak types make invalid states look valid until runtime.
Example
Flags only direct escape hatches (any, Any, as any casts).
Problem
from typing import Any
def notify_contact(raw_contact: Any) -> None:
if raw_contact["active"]:
mailer.send(raw_contact["email"])
Proposed fix
Use a dataclass or another concrete model so callers pass named fields instead of loose keys. A mapping with a typed value such as dict[str, int] is reported only at high strictness. Do not fix this by creating a shallow alias such as UserPayload = dict[str, Any] or UserId = str.
from dataclasses import dataclass
@dataclass(frozen=True)
class UserContact:
email: str
active: bool
def notify(user: UserContact) -> None:
if user.active:
mailer.send(user.email)
Flags escape hatches plus schema-erasing maps and records: bare dict, dict[str, object], Record<string, unknown>.
Problem
from typing import Any
def notify_contact(contact: dict[str, Any]) -> None:
if contact["active"]:
mailer.send(contact["email"])
Proposed fix
Use a dataclass or another concrete model so callers pass named fields instead of loose keys. A mapping with a typed value such as dict[str, int] is reported only at high strictness. Do not fix this by creating a shallow alias such as UserPayload = dict[str, Any] or UserId = str.
from dataclasses import dataclass
@dataclass(frozen=True)
class UserContact:
email: str
active: bool
def notify(user: UserContact) -> None:
if user.active:
mailer.send(user.email)
Flags everything in medium, primitive-only collections and typed-value maps (dict[str, int]), bare unknown params and returns, and shallow aliases.
Problem
EmailAddress = str
def notify_all(addresses: list[EmailAddress]) -> None:
for address in addresses:
mailer.send(address)
Proposed fix
Use a dataclass or another concrete model so callers pass named fields instead of loose keys. A mapping with a typed value such as dict[str, int] is reported only at high strictness. Do not fix this by creating a shallow alias such as UserPayload = dict[str, Any] or UserId = str.
from dataclasses import dataclass
@dataclass(frozen=True)
class UserContact:
email: str
active: bool
def notify(user: UserContact) -> None:
if user.active:
mailer.send(user.email)
Flags only direct escape hatches (any, Any, as any casts).
Problem
function notifyContact(rawContact: any): void {
if (rawContact.active) {
mailer.send(rawContact.email);
}
}
Proposed fix
Replace any with an interface, unknown plus narrowing, or a schema-derived type. Type-guard subjects and predicates are never reported, nor callbacks whose result is unknown; bare unknown params and returns only at high strictness. This rule reads signatures and class fields only: as any and other casts inside a body are type_escape_cast findings. Do not fix this by creating a shallow alias such as type UserPayload = Record<string, any> or type UserId = string.
interface User {
email: string;
active: boolean;
}
function notify(user: User): void {
if (user.active) {
mailer.send(user.email);
}
}
Flags escape hatches plus schema-erasing maps and records: bare dict, dict[str, object], Record<string, unknown>.
Problem
function notifyContact(contact: Record<string, any>): void {
if (contact.active) {
mailer.send(contact.email);
}
}
Proposed fix
Replace any with an interface, unknown plus narrowing, or a schema-derived type. Type-guard subjects and predicates are never reported, nor callbacks whose result is unknown; bare unknown params and returns only at high strictness. This rule reads signatures and class fields only: as any and other casts inside a body are type_escape_cast findings. Do not fix this by creating a shallow alias such as type UserPayload = Record<string, any> or type UserId = string.
interface User {
email: string;
active: boolean;
}
function notify(user: User): void {
if (user.active) {
mailer.send(user.email);
}
}
Flags everything in medium, primitive-only collections and typed-value maps (dict[str, int]), bare unknown params and returns, and shallow aliases.
Problem
type EmailAddress = string;
function notifyAll(addresses: EmailAddress[]): void {
for (const address of addresses) {
mailer.send(address);
}
}
Proposed fix
Replace any with an interface, unknown plus narrowing, or a schema-derived type. Type-guard subjects and predicates are never reported, nor callbacks whose result is unknown; bare unknown params and returns only at high strictness. This rule reads signatures and class fields only: as any and other casts inside a body are type_escape_cast findings. Do not fix this by creating a shallow alias such as type UserPayload = Record<string, any> or type UserId = string.
interface User {
email: string;
active: boolean;
}
function notify(user: User): void {
if (user.active) {
mailer.send(user.email);
}
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.loose_typing]
enabled = true
action = "warning"
strictness = "low"
Flags only direct escape hatches (any, Any, as any casts).
[smells.<lang>.rules.loose_typing]
enabled = true
action = "warning"
strictness = "medium"
Flags escape hatches plus schema-erasing maps and records: bare dict, dict[str, object], Record<string, unknown>.
[smells.<lang>.rules.loose_typing]
enabled = true
action = "warning"
strictness = "high"
Flags everything in medium, primitive-only collections and typed-value maps (dict[str, int]), bare unknown params and returns, and shallow aliases.
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
Type Escape Cast (type_escape_cast)
What it is
A function body silences the type checker (as any, as unknown as T, a bare @ts-ignore or # type: ignore, cast(Any, …)) instead of giving the value a type — the types and the code disagree and nobody looked. Body casts belong to this rule; signatures belong to loose_typing.
Why it's bad
A cast or suppression in a body turns a type error into a runtime surprise: the checker complained, the code was silenced, and no reviewer saw why.
Example
Flags casts to the escape type (as any, cast(Any, …)) and bare @ts-ignore.
Problem
from typing import Any, cast
def order_total(raw: object) -> int:
payload = cast(Any, raw)
return sum(item["amount"] for item in payload["items"])
Proposed fix
Give the value a real type (a dataclass, TypedDict or validated model) or narrow it with isinstance. A # type: ignore that must stay names its error code, as in # type: ignore[attr-defined]. Casts in a TypeGuard function on its own subject are not reported. Signatures belong to loose_typing; this rule owns casts inside bodies.
def order_total(raw: object) -> int:
payload = Order.model_validate(raw)
return sum(item.amount for item in payload.items)
Also flags as unknown as T, bare @ts-expect-error, and # type: ignore without an error code.
Problem
def shipping_label(order: Order) -> str:
return f"{order.parcel.carrier} {order.parcel.tracking_code}" # type: ignore
Proposed fix
Give the value a real type (a dataclass, TypedDict or validated model) or narrow it with isinstance. A # type: ignore that must stay names its error code, as in # type: ignore[attr-defined]. Casts in a TypeGuard function on its own subject are not reported. Signatures belong to loose_typing; this rule owns casts inside bodies.
def order_total(raw: object) -> int:
payload = Order.model_validate(raw)
return sum(item.amount for item in payload.items)
Also flags non-null assertions on call results (find()!).
Problem
def primary_email(user: User) -> str:
return user.primary_email.address # type: ignore
Proposed fix
Give the value a real type (a dataclass, TypedDict or validated model) or narrow it with isinstance. A # type: ignore that must stay names its error code, as in # type: ignore[attr-defined]. Casts in a TypeGuard function on its own subject are not reported. Signatures belong to loose_typing; this rule owns casts inside bodies.
def order_total(raw: object) -> int:
payload = Order.model_validate(raw)
return sum(item.amount for item in payload.items)
Flags casts to the escape type (as any, cast(Any, …)) and bare @ts-ignore.
Problem
export async function orderTotal(res: Response): Promise<number> {
const payload = (await res.json()) as any;
return payload.items.reduce((sum: number, item: { amount: number }) => sum + item.amount, 0);
}
Proposed fix
Parse the value with a schema or narrow it with a type guard instead of as any or as unknown as T. A suppression that must stay is @ts-expect-error with a reason after it. as const and satisfies are never reported, nor are casts of the guarded subject inside a type-predicate function, casts with a rationale comment on their line or the line above, and (at high) non-null calls already checked by an enclosing condition, a has test or a length loop. The type_escape_strictness knob (strictness under [smells.rules.type_escape_cast]) adds non-null assertions on call results at high. Signatures belong to loose_typing; this rule owns casts inside bodies.
export async function orderTotal(res: Response): Promise<number> {
const payload = OrderSchema.parse(await res.json());
return payload.items.reduce((sum, item) => sum + item.amount, 0);
}
Also flags as unknown as T, bare @ts-expect-error, and # type: ignore without an error code.
Problem
export function shippingLabel(order: Order): string {
const parcel = order as unknown as Parcel;
return `${parcel.carrier} ${parcel.trackingCode}`;
}
Proposed fix
Parse the value with a schema or narrow it with a type guard instead of as any or as unknown as T. A suppression that must stay is @ts-expect-error with a reason after it. as const and satisfies are never reported, nor are casts of the guarded subject inside a type-predicate function, casts with a rationale comment on their line or the line above, and (at high) non-null calls already checked by an enclosing condition, a has test or a length loop. The type_escape_strictness knob (strictness under [smells.rules.type_escape_cast]) adds non-null assertions on call results at high. Signatures belong to loose_typing; this rule owns casts inside bodies.
export async function orderTotal(res: Response): Promise<number> {
const payload = OrderSchema.parse(await res.json());
return payload.items.reduce((sum, item) => sum + item.amount, 0);
}
Also flags non-null assertions on call results (find()!).
Problem
export function primaryEmail(user: User): string {
return user.emails.find((email) => email.primary)!.address;
}
Proposed fix
Parse the value with a schema or narrow it with a type guard instead of as any or as unknown as T. A suppression that must stay is @ts-expect-error with a reason after it. as const and satisfies are never reported, nor are casts of the guarded subject inside a type-predicate function, casts with a rationale comment on their line or the line above, and (at high) non-null calls already checked by an enclosing condition, a has test or a length loop. The type_escape_strictness knob (strictness under [smells.rules.type_escape_cast]) adds non-null assertions on call results at high. Signatures belong to loose_typing; this rule owns casts inside bodies.
export async function orderTotal(res: Response): Promise<number> {
const payload = OrderSchema.parse(await res.json());
return payload.items.reduce((sum, item) => sum + item.amount, 0);
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.type_escape_cast]
enabled = true
action = "warning"
strictness = "low"
Flags casts to the escape type (as any, cast(Any, …)) and bare @ts-ignore.
[smells.<lang>.rules.type_escape_cast]
enabled = true
action = "warning"
strictness = "medium"
Also flags as unknown as T, bare @ts-expect-error, and # type: ignore without an error code.
[smells.<lang>.rules.type_escape_cast]
enabled = true
action = "warning"
strictness = "high"
Also flags non-null assertions on call results (find()!).
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
External linter coverage
- ESLint:
@typescript-eslint/no-explicit-any - ESLint:
@typescript-eslint/ban-ts-comment - Ruff:
PGH003
Tuple Packing (tuple_packing)
What it is
Data passed as positional tuples whose fields aren't named — use a named structure so meaning is explicit.
Why it's bad
Positional bundles hide meaning and make the code brittle to reorderings.
Example
Problem
def summarize(user: User) -> tuple[str, str, int]:
return user.name, user.email, user.seats
Proposed fix
Use a named tuple, dataclass, or object with explicit fields.
from dataclasses import dataclass
@dataclass(frozen=True)
class UserSummary:
name: str
email: str
seats: int
def summarize(user: User) -> UserSummary:
return UserSummary(name=user.name, email=user.email, seats=user.seats)
Problem
function summarize(user: User): [string, string, number] {
return [user.name, user.email, user.seats];
}
Proposed fix
Use an interface/object instead of anonymous positional tuple data.
interface UserSummary {
name: string;
email: string;
seats: number;
}
function summarize(user: User): UserSummary {
return { name: user.name, email: user.email, seats: user.seats };
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.tuple_packing]
enabled = true
action = "warning"
max_tuple_return = 2 # allowed positional tuple items in a return
allow_homogeneous = true # allow same-primitive tuples of up to 4 items
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
Defensive Fallback Soup (defensive_fallback)
What it is
Broad error handling and repeated empty defaults hide invalid states — validate the boundary and let unexpected failures remain visible.
Why it's bad
Broad catches plus empty defaults turn contract violations into plausible but wrong data.
Example
Problem
def load_payload(raw):
try:
names = raw.get("names") or []
options = raw.get("options") or {}
return names, options
except:
return [], {}
Proposed fix
Validate once at the boundary, catch specific failures, and preserve unexpected errors.
def load_payload(raw):
try:
return Payload.parse(raw)
except InvalidPayload as error:
raise UserInputError() from error
Problem
function loadPayload(raw: Input) {
try {
const names = raw.names || [];
const options = raw.options || {};
return { names, options };
} catch {
return { names: [], options: {} };
}
}
Proposed fix
Parse once at the boundary, catch specific failures, and preserve unexpected errors.
function loadPayload(raw: unknown): Payload {
return PayloadSchema.parse(raw);
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.defensive_fallback]
enabled = true
action = "warning"
# This detector has no extra threshold knobs.
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | No |
| JS / TS | No |
| Rust | No |
Swallowed Error Sentinel (swallowed_error_sentinel)
What it is
An error handler drops the error and returns a placeholder (null, [], false) or only logs — callers get a plausible value instead of a failure; rethrow or return a typed failure.
Why it's bad
A handler that turns any failure into null, an empty list or a log line hands callers a plausible value; the broken contract surfaces far from its cause, or never.
Example
Flags handlers that return an empty value (null, None, [], false), except around a single JSON parse.
Problem
def load_profile(profile_id):
try:
return client.profiles.get(profile_id)
except Exception as error:
log.warning("profile load failed: %s", error)
return None
Proposed fix
Catch the specific exception you can handle, re-raise the rest with raise ... from error, or return a typed failure the caller must check.
def load_profile(profile_id):
try:
return client.profiles.get(profile_id)
except ClientError as error:
raise ProfileUnavailable(profile_id) from error
Also flags empty-value returns around a single JSON parse.
Problem
import json
def read_settings(text):
try:
return json.loads(text)
except Exception:
return None
Proposed fix
Catch the specific exception you can handle, re-raise the rest with raise ... from error, or return a typed failure the caller must check.
def load_profile(profile_id):
try:
return client.profiles.get(profile_id)
except ClientError as error:
raise ProfileUnavailable(profile_id) from error
Flags handlers that return an empty value (null, None, [], false), except around a single JSON parse.
Problem
async function loadProfile(id: string): Promise<Profile | null> {
try {
return await client.profiles.get(id);
} catch (error) {
console.warn("profile load failed", error);
return null;
}
}
Proposed fix
Rethrow, or return a typed failure ({ ok: false, error } or a Result) so the caller must handle it; keep a tryX(): T | null helper only where missing is the documented contract.
async function loadProfile(id: string): Promise<Result<Profile, LoadError>> {
try {
return ok(await client.profiles.get(id));
} catch (error) {
return err(toLoadError(error));
}
}
Also flags empty-value returns around a single JSON parse.
Problem
function readSettings(text: string) {
try {
return JSON.parse(text);
} catch {
return undefined;
}
}
Proposed fix
Rethrow, or return a typed failure ({ ok: false, error } or a Result) so the caller must handle it; keep a tryX(): T | null helper only where missing is the documented contract.
async function loadProfile(id: string): Promise<Result<Profile, LoadError>> {
try {
return ok(await client.profiles.get(id));
} catch (error) {
return err(toLoadError(error));
}
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.swallowed_error_sentinel]
enabled = true
action = "warning"
strictness = "low"
Flags handlers that return an empty value (null, None, [], false), except around a single JSON parse.
[smells.<lang>.rules.swallowed_error_sentinel]
enabled = true
action = "warning"
strictness = "medium"
Also flags empty-value returns around a single JSON parse.
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
Mutation and State
Mutated Parameter (mutated_parameter)
What it is
The function mutates a caller's argument in place — a hidden side effect; return a new value instead.
Why it's bad
Mutating inputs hides side effects and makes call order matter.
Example
Problem
def add_item(items: list[Item], item: Item) -> list[Item]:
items.append(item)
return items
Proposed fix
Return a new collection or make the mutation explicit in the API name.
def add_item(items: list[Item], item: Item) -> list[Item]:
next_items = list(items)
next_items.append(item)
return next_items
Problem
function addItem(items: Item[], item: Item): Item[] {
items.push(item);
return items;
}
Proposed fix
Return a copied value or make mutation an intentional method on an owner.
function addItem(items: Item[], item: Item): Item[] {
return [...items, item];
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.mutated_parameter]
enabled = true
action = "warning"
include_attributes = false # also flag mutation of fields on parameters
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
External linter coverage
- ESLint:
no-param-reassign
Name Promise Mismatch (name_promise_mismatch)
What it is
A function named like a read (get, is, find, validate, ...) also writes state — rename it to say so or move the write to the caller.
Why it's bad
Callers trust a read-named function to be safe to call twice, cache, or skip; a hidden write breaks all three.
Example
Flags only database, network and scheduling writes.
Problem
def get_invoice(db: Database, invoice_id: str) -> Invoice | None:
invoice = db.invoices.find(invoice_id)
if invoice is None:
db.invoices.insert({"id": invoice_id, "status": "draft"})
return invoice
Proposed fix
Rename it to name the write (get_or_create_invoice), or return the data and let the caller write.
def get_or_create_invoice(db: Database, invoice_id: str) -> Invoice:
invoice = db.invoices.find(invoice_id)
if invoice is not None:
return invoice
return db.invoices.insert({"id": invoice_id, "status": "draft"})
Also flags mutating calls and assignments on parameters, own members and module-level names; skips caches and lazy initialisation.
Problem
class TaskQueue:
def __init__(self) -> None:
self.tasks: list[Task] = []
def get_next_task(self) -> Task | None:
next_task = self.tasks[0] if self.tasks else None
self.tasks.remove(next_task)
return next_task
Proposed fix
Rename it to name the write (get_or_create_invoice), or return the data and let the caller write.
def get_or_create_invoice(db: Database, invoice_id: str) -> Invoice:
invoice = db.invoices.find(invoice_id)
if invoice is not None:
return invoice
return db.invoices.insert({"id": invoice_id, "status": "draft"})
Also flags cache fills and lazy initialisation, and treats load and fetch names as reads.
Problem
class RateTable:
def __init__(self) -> None:
self.source: RateSource | None = None
def get_source(self) -> RateSource:
if self.source is None:
self.source = RateSource()
return self.source
Proposed fix
Rename it to name the write (get_or_create_invoice), or return the data and let the caller write.
def get_or_create_invoice(db: Database, invoice_id: str) -> Invoice:
invoice = db.invoices.find(invoice_id)
if invoice is not None:
return invoice
return db.invoices.insert({"id": invoice_id, "status": "draft"})
Flags only database, network and scheduling writes.
Problem
export async function getInvoice(db: Database, invoiceId: string): Promise<Invoice | null> {
const invoice = await db.invoices.find(invoiceId);
if (!invoice) {
await db.invoices.insert({ id: invoiceId, status: "draft" });
}
return invoice;
}
Proposed fix
Rename it to name the write (getOrCreateInvoice), or return the data and let the caller write.
export async function getOrCreateInvoice(db: Database, invoiceId: string): Promise<Invoice> {
const invoice = await db.invoices.find(invoiceId);
if (invoice) {
return invoice;
}
return db.invoices.insert({ id: invoiceId, status: "draft" });
}
Also flags mutating calls and assignments on parameters, own members and module-level names; skips caches and lazy initialisation.
Problem
export class TaskQueue {
private tasks: Task[] = [];
getNextTask(): Task | undefined {
const next = this.tasks[0];
this.tasks.splice(0, 1);
return next;
}
}
Proposed fix
Rename it to name the write (getOrCreateInvoice), or return the data and let the caller write.
export async function getOrCreateInvoice(db: Database, invoiceId: string): Promise<Invoice> {
const invoice = await db.invoices.find(invoiceId);
if (invoice) {
return invoice;
}
return db.invoices.insert({ id: invoiceId, status: "draft" });
}
Also flags cache fills and lazy initialisation, and treats load and fetch names as reads.
Problem
const rates = new Map<string, number>();
export function getRate(currency: string): number {
const known = rates.get(currency);
if (known !== undefined) {
return known;
}
const rate = currency.length / 10;
rates.set(currency, rate);
return rate;
}
Proposed fix
Rename it to name the write (getOrCreateInvoice), or return the data and let the caller write.
export async function getOrCreateInvoice(db: Database, invoiceId: string): Promise<Invoice> {
const invoice = await db.invoices.find(invoiceId);
if (invoice) {
return invoice;
}
return db.invoices.insert({ id: invoiceId, status: "draft" });
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.name_promise_mismatch]
enabled = true
action = "warning"
strictness = "low"
read_prefixes = ["get", "is", "has", "find", "list", "validate", "check"] # name prefixes that promise a read
write_methods = ["insert", "patch", "upsert", "delete", "update", "set", "save", "commit", "post", "put"] # method names that count as writes on outside receivers (each language adds its own mutators)
Flags only database, network and scheduling writes.
[smells.<lang>.rules.name_promise_mismatch]
enabled = true
action = "warning"
strictness = "medium"
read_prefixes = ["get", "is", "has", "find", "list", "validate", "check"] # name prefixes that promise a read
write_methods = ["insert", "patch", "upsert", "delete", "update", "set", "save", "commit", "post", "put"] # method names that count as writes on outside receivers (each language adds its own mutators)
Also flags mutating calls and assignments on parameters, own members and module-level names; skips caches and lazy initialisation.
[smells.<lang>.rules.name_promise_mismatch]
enabled = true
action = "warning"
strictness = "high"
read_prefixes = ["get", "is", "has", "find", "list", "validate", "check"] # name prefixes that promise a read
write_methods = ["insert", "patch", "upsert", "delete", "update", "set", "save", "commit", "post", "put"] # method names that count as writes on outside receivers (each language adds its own mutators)
Also flags cache fills and lazy initialisation, and treats load and fetch names as reads.
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
Reassigned Parameter (reassigned_parameter)
What it is
A parameter is rebound to a new value inside the body — confusing; use a separate local.
Why it's bad
Rebinding a parameter muddies the original meaning of the value.
Example
Problem
def normalize(name: str) -> str:
name = name.strip()
return name.title()
Proposed fix
Introduce a local variable for the transformed value.
def normalize(name: str) -> str:
cleaned_name = name.strip()
return cleaned_name.title()
Problem
function normalize(name: string): string {
name = name.trim();
return name.toUpperCase();
}
Proposed fix
Use a separate const for each semantic step.
function normalize(name: string): string {
const cleanedName = name.trim();
return cleanedName.toUpperCase();
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.reassigned_parameter]
enabled = true
action = "warning"
# This detector has no extra threshold knobs.
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | No |
| JS / TS | No |
| Rust | No |
External linter coverage
- ESLint:
no-param-reassign
Split Variable (split_variable)
What it is
One local is reassigned to mean different things at different points — use distinct, single-purpose bindings.
Why it's bad
A variable with multiple meanings is a trap for both readers and debuggers.
Example
Problem
def invoice_total(invoice: Invoice) -> float:
result = sum(item.price for item in invoice.items)
result = result - invoice.discount
result = result + invoice.tax
return result
Proposed fix
Use separate locals named for each meaning.
def invoice_total(invoice: Invoice) -> float:
subtotal = sum(item.price for item in invoice.items)
discounted_total = subtotal - invoice.discount
return discounted_total + invoice.tax
Problem
function invoiceTotal(invoice: Invoice): number {
let result = sum(invoice.items.map((item) => item.price));
result = result - invoice.discount;
result = result + invoice.tax;
result = roundCurrency(result);
return result;
}
Proposed fix
Prefer distinct const bindings for distinct concepts.
function invoiceTotal(invoice: Invoice): number {
const subtotal = sum(invoice.items.map((item) => item.price));
const discountedTotal = subtotal - invoice.discount;
return discountedTotal + invoice.tax;
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.split_variable]
enabled = true
action = "warning"
min_assigns = 2 # assignments to one local before flagging
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | No |
| JS / TS | No |
| Rust | No |
Performance
Nested Loop (nested_loop)
What it is
A loop is nested directly or through a helper called inside a loop — work grows multiplicatively; combine passes or pre-index the data.
Why it's bad
Costs grow faster than the data and the code gets hard to flatten.
Example
Problem
def find_matches(users: list[User], orders: list[Order]) -> list[tuple[User, Order]]:
matches = []
for user in users:
for order in orders:
if order.user_id == user.id:
matches.append((user, order))
return matches
Proposed fix
Pre-index one side, combine passes, or use a clearer iterator pipeline.
def find_matches(users: list[User], orders: list[Order]) -> list[tuple[User, Order]]:
orders_by_user = {order.user_id: order for order in orders}
matches = []
for user in users:
order = orders_by_user.get(user.id)
if order is not None:
matches.append((user, order))
return matches
Problem
function findMatches(users: User[], orders: Order[]): Array<[User, Order]> {
const matches: Array<[User, Order]> = [];
for (const user of users) {
for (const order of orders) {
if (order.userId === user.id) {
matches.push([user, order]);
}
}
}
return matches;
}
Proposed fix
Build a lookup map or flatten the data before iterating.
function findMatches(users: User[], orders: Order[]): Array<[User, Order]> {
const ordersByUser = indexOrdersByUser(orders);
const matches: Array<[User, Order]> = [];
for (const user of users) {
const order = ordersByUser.get(user.id);
if (order) {
matches.push([user, order]);
}
}
return matches;
}
function indexOrdersByUser(orders: Order[]): Map<string, Order> {
const ordersByUser = new Map<string, Order>();
for (const order of orders) {
ordersByUser.set(order.userId, order);
}
return ordersByUser;
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.nested_loop]
enabled = true
action = "warning"
# This detector has no extra threshold knobs.
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | No |
| JS / TS | No |
| Rust | No |
N+1 Loop Call (n_plus_one_call)
What it is
An external-looking call runs once per loop item — prefer a bulk query/request or prefetch so work scales by batch, not item.
Why it's bad
A per-item call can explode runtime and hit the backend one request at a time.
Example
Problem
def load_profiles(api: ProfileApi, users: list[User]) -> list[Profile]:
profiles = []
for user in users:
profiles.append(api.fetch(user.id))
return profiles
Proposed fix
Batch-load or prefetch the related data before the loop.
def load_profiles(api: ProfileApi, users: list[User]) -> list[Profile]:
ids = [user.id for user in users]
profiles = api.fetch_many(ids)
return [profiles[user_id] for user_id in ids]
Problem
function loadProfiles(api: ProfileApi, users: User[]): Profile[] {
const profiles = [];
for (const user of users) {
profiles.push(api.fetch(user.id));
}
return profiles;
}
Proposed fix
Use a bulk endpoint/query or prefetch into a map before rendering.
async function loadProfiles(api: ProfileApi, users: User[]): Promise<Profile[]> {
const ids = users.map((user) => user.id);
const profiles = await api.fetchMany(ids);
return ids.map((id) => profiles[id]);
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.n_plus_one_call]
enabled = true
action = "warning"
callees = [] # globs for base.method or plain calls that hit a backend
max_constant_bound = 10 # loops with at most this literal trip count are skipped
await_in_loop = false # also flag awaited per-item calls on outside receivers
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | No |
| JS / TS | No |
| Rust | No |
Repeated Iteration (repeated_iteration)
What it is
The same collection gets three or more passes, two loops, or two passes where one folds it (reduce, sum, filter(..).length), in one scope with nothing forcing them apart — fuse the passes so the data is scanned once. Field projections, rendered lists and Set/Map indexes built from a pass do not count.
Why it's bad
The same collection gets scanned over and over when one pass would do.
Example
Problem
def summarize(items: Items) -> Summary:
has_items = any(items)
total = sum(items)
return Summary(has_items=has_items, total=total)
Proposed fix
Fuse compatible passes or cache the intermediate result.
def summarize(items: Items) -> Summary:
has_items = False
total = 0
for item in items:
has_items = True
total += item
return Summary(has_items=has_items, total=total)
Problem
function summarize(items: Items): Summary {
const paid = items.filter((item) => item.paid).length;
const total = items.reduce((sum, item) => sum + item.price, 0);
return { paid, total };
}
Proposed fix
Combine loops when the operations share the same traversal.
function summarize(items: Items): Summary {
let paid = 0;
let total = 0;
for (const item of items) {
if (item.paid) {
paid += 1;
}
total += item.price;
}
return { paid, total };
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.repeated_iteration]
enabled = true
action = "warning"
include_tests = false # also flag test code and assertion arguments
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
Sort In Loop (sort_in_loop)
What it is
A collection is sorted inside a loop — hoist sorting or maintain ordered data to avoid repeated O(n log n) work.
Why it's bad
Repeated sorts turn a small loop into a surprisingly expensive one.
Example
Problem
def grouped_names(groups: Groups) -> Names:
names = []
for group in groups:
names.extend(group.names)
names.sort()
return names
Proposed fix
Sort once before the loop or keep data ordered as it is built.
def grouped_names(groups: Groups) -> Names:
names = []
for group in groups:
names.extend(group.names)
names.sort()
return names
Problem
function groupedNames(groups: Groups, names: string[]): string[] {
const result: string[] = [];
for (const group of groups) {
names.sort();
result.push(group.name);
}
return result;
}
Proposed fix
Hoist sorting out of the loop or maintain an ordered structure.
function groupedNames(groups: Groups): string[] {
const names: string[] = [];
for (const group of groups) {
names.push(...group.names);
}
names.sort();
return names;
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.sort_in_loop]
enabled = true
action = "warning"
# This detector has no extra threshold knobs.
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
Other
Dependency Reimplementation (dependency_reimplementation)
What it is
A small helper re-writes a utility that a declared dependency or the standard library already provides — call the provider instead of keeping a private copy.
Why it's bad
Each private copy of a stock utility is one more thing to test and maintain, and copies drift: one swaps the bounds, another drops the last chunk.
Example
Problem
def chunks(items, size):
return [items[start : start + size] for start in range(0, len(items), size)]
print(chunks(["a", "b", "c"], 2))
Proposed fix
Call the standard-library or declared-package function the message names. List in-house utility packages in providers so their functions count too.
from itertools import batched
print(list(batched(["a", "b", "c"], 2)))
Problem
// package.json lists es-toolkit
function unique<T>(items: T[]): T[] {
return Array.from(new Set(items));
}
console.log(unique(["a", "a", "b"]));
Proposed fix
Import the function the message names from the package package.json already lists. List in-house utility packages in providers so they count too.
import { uniq } from "es-toolkit";
console.log(uniq(["a", "a", "b"]));
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.dependency_reimplementation]
enabled = true
action = "warning"
providers = [] # in-house packages whose same-named functions count as providers
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
Fixture Echo Assertion (fixture_echo_assertion)
What it is
A test assertion cannot fail: both sides are the same expression, equal literals, or a field read from a fixture and its unchanged alias; or, with include_mock_only, the test only checks that a mock it programmed was called — assert on what the code under test produced.
Why it's bad
An assertion that compares a value with itself passes whatever the code under test does, so the test reports coverage it does not have.
Example
Problem
def rename(label: str) -> str:
return label.strip()
def test_keeps_label() -> None:
label = rename(" box ")
assert label == label
Proposed fix
Compare the result of the call under test with an independently written expected value.
def rename(label: str) -> str:
return label.strip()
def test_keeps_label() -> None:
assert rename(" box ") == "box"
Problem
import assert from "node:assert/strict";
import test from "node:test";
function rename(label: string): string {
return label.trim();
}
test("keeps the label", () => {
const label = rename(" box ");
assert.equal(label, label);
});
Proposed fix
Pass the result of the call under test to expect and an independently written value to the matcher; for mocks, also assert on the value the subject returned.
import assert from "node:assert/strict";
import test from "node:test";
function rename(label: string): string {
return label.trim();
}
test("keeps the label", () => {
assert.equal(rename(" box "), "box");
});
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.fixture_echo_assertion]
enabled = true
action = "warning"
include_mock_only = false # also flag tests whose only checks are was-called checks on mocks they programmed
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
Mocked Subject (mocked_subject)
What it is
A test file mocks the module its subject under test is imported from, so the test exercises the double instead of the code — mock collaborators, keep the subject real.
Why it's bad
A test that replaces the module it claims to test checks the double's canned answer; it stays green when the real code breaks.
Example
Flags a mock whose module resolves to the import a test calls its subject through.
Problem
from unittest.mock import patch
from shop import pricing
@patch("shop.pricing.quote", return_value=42)
def test_quote(mocked):
assert pricing.quote(2) == 42
Proposed fix
Patch the collaborators the subject calls, or patch with wraps= to keep the real function.
from shop import pricing
def test_quote(mocker):
mocker.patch("shop.pricing.fetch_rate", return_value=2)
assert pricing.quote(2) == 4
Also flags a mocked module named like the test file, and patches of an attribute on an imported object the test calls.
Problem
from unittest.mock import patch
from shop.pricing import Pricing
def test_quote():
with patch.object(Pricing, "rate", return_value=2):
assert Pricing().quote(2) == 4
Proposed fix
Patch the collaborators the subject calls, or patch with wraps= to keep the real function.
from shop import pricing
def test_quote(mocker):
mocker.patch("shop.pricing.fetch_rate", return_value=2)
assert pricing.quote(2) == 4
Flags a mock whose module resolves to the import a test calls its subject through.
Problem
import { quote } from "./pricing";
vi.mock("./pricing", () => ({ quote: vi.fn(() => 42) }));
it("quotes", () => {
expect(quote({ qty: 2 })).toBe(42);
});
Proposed fix
Mock the collaborators the subject imports, or keep the real module with importActual / importOriginal.
import { quote } from "./pricing";
vi.mock("./rates", () => ({ fetchRate: vi.fn(() => 1.5) }));
it("quotes", () => {
expect(quote({ qty: 2 })).toBe(3);
});
Also flags a mocked module named like the test file, and patches of an attribute on an imported object the test calls.
Problem
// pricing.example.test.ts: the mocked module shares the test file's name.
import { render } from "./receipt";
vi.mock("./pricing");
it("renders the receipt", () => {
expect(render({ qty: 2 })).toContain("4");
});
Proposed fix
Mock the collaborators the subject imports, or keep the real module with importActual / importOriginal.
import { quote } from "./pricing";
vi.mock("./rates", () => ({ fetchRate: vi.fn(() => 1.5) }));
it("quotes", () => {
expect(quote({ qty: 2 })).toBe(3);
});
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.mocked_subject]
enabled = true
action = "warning"
strictness = "medium"
Flags a mock whose module resolves to the import a test calls its subject through.
[smells.<lang>.rules.mocked_subject]
enabled = true
action = "warning"
strictness = "high"
Also flags a mocked module named like the test file, and patches of an attribute on an imported object the test calls.
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
Redundant Wrapper Chain (redundant_wrapper_chain)
What it is
A local function forwards arguments unchanged without adding behavior — remove the wrapper or allow a deliberate depth in configuration.
Why it's bad
An unchanged forwarding layer adds indirection without behavior and makes navigation harder.
Example
Problem
def normalize(value: int) -> int:
return value + 1
def middle(value: int) -> int:
return normalize(value)
def outer(value: int) -> int:
return middle(value)
result = outer(2)
Proposed fix
Remove pass-through functions, or raise max_depth when a deliberate boundary is worth keeping.
def normalize(value: int) -> int:
return value + 1
result = normalize(2)
Problem
function normalize(value: number): number {
return value + 1;
}
function middle(value: number): number {
return normalize(value);
}
function outer(value: number): number {
return middle(value);
}
const result = outer(2);
console.log(result);
Proposed fix
Remove pass-through functions, or raise max_depth when a deliberate boundary is worth keeping.
function normalize(value: number): number {
return value + 1;
}
const result = normalize(2);
console.log(result);
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.redundant_wrapper_chain]
enabled = true
action = "warning"
max_depth = 0 # allowed depth before flagging
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
Shipped Stub (stub_shipped)
What it is
A production function whose whole body is a placeholder (pass, raise NotImplementedError, a "not implemented" throw) — it fails the first caller who reaches it.
Why it's bad
Scaffolding written for later ships as a function that only fails, and the first user on that path hits the error at runtime. Unreferenced stubs are also reported as dead code. Keep deliberate scaffolding folders out with [smells] exclude.
Example
Problem
def export_ledger(org_id: str) -> bytes:
raise NotImplementedError
report = export_ledger("acme")
Proposed fix
Implement the function, delete it with its callers, or declare it with @abstractmethod on an ABC/Protocol.
def export_ledger(org_id: str) -> bytes:
return f"ledger,{org_id}\n".encode()
report = export_ledger("acme")
Problem
export async function exportLedger(orgId: string): Promise<Blob> {
// TODO: implement CSV export
throw new Error("not implemented");
}
Proposed fix
Implement the function, delete it with its callers, or declare it abstract or as an interface member. Tune the thrown messages with markers.
export async function exportLedger(orgId: string): Promise<Blob> {
return new Blob([`ledger,${orgId}\n`], { type: "text/csv" });
}
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.stub_shipped]
enabled = true
action = "warning"
markers = ["not implemented" # thrown-error messages that mark a stub, matched as whole words
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | Yes |
| JS / TS | Yes |
| Rust | Yes |
Weak Test Oracle (weak_test_oracle)
What it is
A test has no direct assertion, matcher, or expected exception — check whether it verifies behavior or only executes code.
Why it's bad
A test that only executes code can pass when behavior is wrong.
Example
Problem
def save(value: int) -> int:
return value
def test_saves() -> None:
saved = save(1)
save(saved)
Proposed fix
Assert an observable result or expected exception.
def save(value: int) -> int:
return value
def test_saves() -> None:
assert save(1) == 1
Problem
import test from "node:test";
function save(value: number): number {
return value;
}
test("saves value", () => {
save(1);
});
Proposed fix
Assert the result, state, or expected rejection with a matcher.
import assert from "node:assert/strict";
import test from "node:test";
function save(value: number): number {
return value;
}
test("saves value", () => {
assert.equal(save(1), 1);
});
Tune It
Replace <lang> with python, javascript, typescript, or rust.
[smells.<lang>.rules.weak_test_oracle]
enabled = true
action = "warning"
assertion_functions = ["expect*", "assert*", "verify*"] # globs for imported helpers that count as checks
Default enabled state
| Language | Enabled by default |
|---|---|
| Python | No |
| JS / TS | No |
| Rust | No |