Case file S5

Inline-First

Don't introduce names for nothing.

A binding justifies itself when its name carries information the expression does not. Otherwise it is noise: a hop the reader has to make for nothing.

ByAdam LewisPublished3 May 2026Revised13 Oct 2026Reading12 minVersionv1.2ConfidenceHigh
§0b

Opinion

Many of the bindings I see in review do not earn their existence. const userName = user.name; followed by a single use of userName on the next line. The name adds nothing user.name does not. Fowler presents the move directly: Inline Variable is the inverse of Extract Variable.6 Extract when the expression hides intent. Inline when the binding adds nothing the expression did not already say.

The other side of the same continuum is the magic-number case. A function that retries three times and waits 5000 milliseconds between attempts is harder to read than the same function with MAX_RETRIES = 3 and BACKOFF_MS = 5000. The literal carries meaning the expression cannot and the meaning is exactly what the constant's name records.2 Same principle, opposite direction: extract when the literal hides intent. Inline when the binding hides nothing.

The third case is the immediate-return alias. const result = compute(); return result; is two lines doing the job of one. The alias does not narrow the type, name a stage, or carry intent. It just gives the reader an extra hop. SonarJS catches this directly with prefer-immediate-return. The rule is in the Prickles repo's lint config and it has paid for itself many times over.3 Pair the rule with F2 Intention-Revealing Names: when the name carries meaning, keep it, and when it does not, drop it.

Copy a note and link

Grab this short comment and drop it into a PR comment or an LLM chat to prompt the right change.

A binding justifies itself when its name carries information the expression does not. Inline first. Extract when a name is needed. Three settled cases: literals with meaning earn a const NAME. Single-use trivial expressions stay inline. Values computed and immediately returned drop the alias.

/tenet/inline-first/S5
Copy for paste
§0c

AI eyes only

Rule: do not introduce names for nothing. A binding justifies itself when its name carries information the expression does not.

Reject: const userName = user.name; followed by a single use of userName. Reject: extracting a one-line helper that has one caller and a name no clearer than the body. Reject: aliases that just restate the right-hand side.

Generate: inline single-use trivial expressions. Bind only when the name documents intent (e.g. const isAdmin = user.role === "admin") or the expression is reused.

Diagnostic: read the binding aloud. If the LHS only restates the RHS, drop the binding.

§0d

Why?

  • Reduces the number of hops the reader has to make. A function whose bindings all earn their existence reads top-to-bottom. A function whose bindings are noise reads as a sequence of chained dictionary lookups.
  • Functions get shorter without losing meaning. Pairs cleanly with S2 Cyclomatic Caps: a function that fails the line cap often passes once the noise bindings are inlined.
  • The literal-with-meaning case earns a named constant. The function reads as MAX_RETRIES rather than 3 and the constant moves to the top of the module where everyone can find it.
  • Coding agents meet the same gate as everyone else. With prefer-immediate-return set to error, an immediate-return alias written by an agent fails the lint, and the change does not pass until the alias is inlined.
  • Cuts review cost. Reviewers stop arguing about whether userName earns its existence. The lint flags it and the conversation is “does the name carry information?” instead of “is this binding necessary?”
  • Hands the naming conversation to F2 Intention-Revealing Names. The binding justifies itself when the name does: F2 picks the names that earn keep; Inline-First removes the ones that do not.
  • Improves diff quality. Renaming a noise binding is two lines. Inlining and re-extracting with a real name is the same diff with the meaning of the rename made obvious. Reviewers catch more in the second form than the first.
The receipts
Origins, quoted passages, evidence, the strongest counter-argument and the reply.
§1

Origins

Martin Fowler catalogued Extract Variable and Inline Variable as inverses in the second edition of Refactoring.1 Ch. 6 “A First Set of Refactorings” pairs the two moves directly: Extract Variable when an expression is hard to follow and a name would clarify it; Inline Variable when a binding is in the way of further refactoring or when it does not carry information the expression does not. The two moves are one continuum. Engineers who read the chapter come away with one rule, not two.

The inline side of the same rule was named long before Refactoring. Brian Kernighan and P. J. Plauger's The Elements of Programming Style (1974) gave it as Rule 4, a warning against temporary variables.5 McConnell's Code Complete §12.7 holds the magic-number side under the heading “Named Constants”.2 The rule is older than most working engineers. The editorial decision is which side of the line a given literal falls on.

The immediate-return side of the rule lives in the SonarJS rule sonarjs/prefer-immediate-return.3 The rule catches the const result = compute(); return result; pattern directly. SonarSource's rationale is exactly this tenet's: the alias carries no information and adds a hop. Same continuum, different surface.

The principle the three sides share is older still. Edsger Dijkstra's “The Humble Programmer” (1972 ACM Turing Award lecture) named the instinct: a programmer's job is to manage the cognitive complexity the code presents to the reader. Every binding is a hop; every hop has a cost. Every hop has to earn its existence by offering meaning the reader did not have. Inline-First is the operational form of that instinct.

§2

Quotes

Variables provide names for expressions within a function, and as such they are usually a Good Thing. But sometimes, the name doesn't really communicate more than the expression itself. At other times, you may find that a variable gets in the way of refactoring the neighboring code. In these cases, it can be useful to inline the variable.

Martin Fowler · Refactoring, 2nd ed. (2018), Inline Variable

Expressions can become very complex and hard to read. In such situations, local variables may help break the expression down into something more manageable. In particular, they give me an ability to name a part of a more complex piece of logic.

Martin Fowler · Refactoring, 2nd ed. (2018), Extract Variable

Magic numbers are literal numbers, such as 100 or 47524, that appear in the middle of a program without explanation. If you program in a language that supports named constants, use them instead.

Steve McConnell · Code Complete 2e (2004), ch. 12

Avoid too many temporary variables.

Brian W. Kernighan & P. J. Plauger · The Elements of Programming Style (1974)
§3

Evidence

16 external sources, ranked by author authority. The first five are the canon; expand to see the rest, including the qualifiers and the named opposers. Each links out to its primary source.

  1. 01
    Martin Fowler · 2018
    The canonical statement of the continuum. Two moves, one rule: extract when a name would clarify. Inline when the binding adds nothing the expression does not.
  2. 02
    Martin Fowler · 2018
    The operational form of the inline-first side. Replace the binding with the expression at every use. Remove the declaration. Run the tests.
  3. 03
    Martin Fowler · 2018
    The inverse move. Used when an expression is hard to read and a name would clarify intent. The two halves of one rule.
  4. 04
    Steve McConnell · 2004
    The literal-with-meaning side. Named constants pay for themselves. Magic numbers do not. The canonical reference for the rule.
  5. 05
    Brian W. Kernighan & P. J. Plauger · 1974, 1978
    “Avoid too many temporary variables.” The inline side of the continuum, named in the 1970s. The half-century-old precursor of the lint config.
  1. 06
    Kent Beck · 1996
    The binding-as-documentation case. Beck argues that intermediate bindings sometimes earn their existence by carrying meaning. The qualifier the canon's reply pairs with F2.
  2. 07
    John Ousterhout · 2021
    “Comments Should Describe Things that Aren't Obvious from the Code.” Ousterhout's parallel argument: a binding is a kind of comment. It should carry meaning the expression does not.
  3. 08
    ESLint maintainers · 2015 onwards
    The canonical lint surface for the named-constants side. Configurable allowlist for trivial values (0, 1, -1).
  4. 09
    SonarSource · 2018 onwards
    The immediate-return side of the continuum. Catches `const x = compute(); return x;` directly. Auto-fixable. The third side of the rule, codified.
  5. 10
    Alexander Shvets · 2014 onwards
    Worked example with code samples. The teaching version of McConnell's §12.7. The named-constants side made operational.
  6. 11
    Pylint project · 2003 onwards
    Python equivalent of the magic-numbers rule. The Pylint reference; Ruff inherits it under the same identifier.
  7. 12
    Astral / Charlie Marsh · 2022 onwards
    Modern Python surface for the same rule. Single binary, single TOML block. The right choice for new projects.
  8. 13
    Astral / Charlie Marsh · 2022 onwards
    Python equivalent of `prefer-immediate-return`. The same continuum, different surface.
  9. 14
    PMD project · 2002 onwards
    Java surface for the named-constants side. Catches the magic-number-in-condition pattern that often surfaces during code review.
  10. 15
    David Thomas & Andrew Hunt · 1999, 2019
    “Don't program by coincidence.” The larger instinct: every binding has to earn its meaning and a literal that survives the lint has to carry intent the constant's name records.
  11. 16
    Edsger Dijkstra · 1972
    The principle underneath the rule. A programmer's job is to manage the cognitive load the code presents to the reader. Every binding is a hop, every hop has a cost, every hop has to earn its existence.

Sixteen sources, two stances. The supporters cluster on the continuum itself: Fowler's Extract Variable / Inline Variable pair, McConnell's named-constants chapter, and Kernighan & Plauger's Rule 4. The qualifiers further down hold the “sometimes the binding is the documentation” reading. There is no substantive opposition. The disagreement is about which side of the continuum a given binding falls on.

§4

Examples

Viewing: TypeScript.
Avoid

Before a single-use alias that names nothing. sonarjs/prefer-immediate-return fires.

Fileformat-invoice.ts
function formatTaxableHedgehogInvoice(invoice: HedgehogInvoice): string {
  const taxableAmount = computeBaseTax(invoice);
  return formatAmount(taxableAmount);
}
Prefer

After the binding is gone. The expression returns directly.

Fileformat-invoice.ts
function formatTaxableHedgehogInvoice(invoice: HedgehogInvoice): string {
  return formatAmount(computeBaseTax(invoice));
}
§4b

Enforcement

Viewing: TypeScript.

Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.

RuleToolCatches
@typescript-eslint/no-magic-numberstypescript-eslintliterals used directly in code where a named constant would carry intent. Extends the core rule, which must be turned off, with options that let enum members, numeric literal types, type indexes and readonly class properties through. The canon also lets 0, 1 and -1, array indexes and default values through, and enforceConst: true requires the named binding to be a const.
sonarjs/prefer-immediate-returneslint-plugin-sonarjsthe const x = compute(); return x; pattern. Auto-fixable: the alias drops and the function reads as the expression.
sonarjs/no-redundant-booleaneslint-plugin-sonarjsboolean literals used where the expression already evaluates to the same boolean. The shape that hides behind unnecessary intermediate bindings.
sonarjs/no-gratuitous-expressionseslint-plugin-sonarjsexpressions whose value is already known at compile time: usually the symptom of a redundant intermediate binding that should be inlined.
eslint.config.mjsconfiguration snippet
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';
import sonarjs from 'eslint-plugin-sonarjs';

export default defineConfig({
  files: ['**/*.{ts,tsx}'],
  extends: [tseslint.configs.base],
  plugins: { sonarjs },
  rules: {
    'no-magic-numbers': 'off',
    '@typescript-eslint/no-magic-numbers': ['error', {
      ignore: [-1, 0, 1],
      ignoreArrayIndexes: true,
      ignoreDefaultValues: true,
      enforceConst: true,
      ignoreEnums: true,
      ignoreNumericLiteralTypes: true,
      ignoreReadonlyClassProperties: true,
      ignoreTypeIndexes: true,
    }],
    'sonarjs/prefer-immediate-return': 'error',
    'sonarjs/no-redundant-boolean': 'error',
    'sonarjs/no-gratuitous-expressions': 'error',
  }
});
§4c

AI rules

File.cursor/rules/s5-inline-first.mdc
---
alwaysApply: true
---

## Prickles S5: Inline-First

Inline first. Extract when a name is needed. A binding justifies itself when its name carries information the expression does not.

Three settled cases. Literals with meaning earn a `const NAME` so the meaning is recorded once and cited many times. Single-use trivial expressions stay inline. Values computed and immediately returned drop the alias.

When the lint flags an immediate-return alias or a magic number, do the rewrite. The rule is the conversation, not the verdict.

Refuse to introduce a name that does not narrow a type, name a stage, or carry intent. The default is the expression. The name has to earn its existence.

Repo layout, CI, and ESLint wiring for these paths live on /implementation, not repeated on every tenet.

§5

Counter-argument

Counter

The strongest steelman is the “binding-as-documentation” school's.4 Kent Beck and Steve McConnell both argue that intermediate bindings are sometimes the cheapest way to document the meaning of an expression: const taxableAmount = grossAmount - exemption; reads better than the inline arithmetic even when taxableAmount is used once. The reply is that the canon agrees: the rule is “extract when the name carries information.” The disagreement is about whether taxableAmount carries information that grossAmount - exemption does not and that judgement is exactly what the code-review conversation is for. The lint surfaces the candidate. The reviewer decides.

§6

Counter-argument retort

Reply

Beck and McConnell are right that the binding is sometimes the documentation and the canon does not dispute that.4 The rule is not “always inline”; it is “inline first, extract when a name is needed.” The default direction matters because the cost is asymmetric: noise bindings accumulate unnoticed and an extra five lines of unnecessary aliases is the reason the function fails the cap on S2 Cyclomatic Caps. The lint surfaces the candidate. The reviewer decides whether the binding carries meaning the expression does not.

The honest residue is the type-narrowing case. In TypeScript and Python, a binding sometimes narrows a union or strips an optional and the binding is load-bearing for the type checker even when its name adds nothing. The reply is that the type narrowing is the meaning the binding carries: the linter that prefers immediate return knows about this and respects type-narrowing bindings; the rule is not breaking the type system.

The genuine residue is the “intermediate name as comment” case Beck names. The canon's answer: pair Inline-First with F2 Intention-Revealing Names. The binding stays when its name is doing something: taxableAmount over grossAmount - exemption passes the test; userName over user.name does not. The lint surfaces both. The reviewer decides which side of the continuum each binding falls on. The rule is the conversation, not the verdict.

The principle is one continuum: extract when the name carries information. Inline when it does not. Three sub-rules, one rule. The lint is the trigger, the review is the conversation and the result is the function whose bindings all justify themselves.

§7

Notes

  1. [1]Martin Fowler: Refactoring, 2nd ed. (Addison-Wesley, 2018), ch. 6 “A First Set of Refactorings”. Extract Variable and Inline Variable as inverses: one continuum, two directions.
  2. [2]Steve McConnell: Code Complete, 2nd ed. (Microsoft Press, 2004), §12.7 “Named Constants”. The literal-with-meaning side of the rule. Named constants pay for themselves and magic numbers do not.
  3. [3]SonarSource: ESLint plugin SonarJS: `prefer-immediate-return` rule. Catches `const x = compute(); return x;` patterns directly, the linter form of the immediate-return side of the continuum.
Disagree? Found a hole in the argument? Take issue with this tenet →
Last revised: 2026-10-13