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.
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
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.
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_RETRIESrather than3and 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-returnset 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
userNameearns 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.
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.
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.
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.
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.
Avoid too many temporary variables.
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.
- 01The 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.
- 02The operational form of the inline-first side. Replace the binding with the expression at every use. Remove the declaration. Run the tests.
- 03The inverse move. Used when an expression is hard to read and a name would clarify intent. The two halves of one rule.
- 04The literal-with-meaning side. Named constants pay for themselves. Magic numbers do not. The canonical reference for the rule.
- 05“Avoid too many temporary variables.” The inline side of the continuum, named in the 1970s. The half-century-old precursor of the lint config.
- 06The 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.
- 07“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.
- 08ESLint: `no-magic-numbers`SupportsThe canonical lint surface for the named-constants side. Configurable allowlist for trivial values (0, 1, -1).
- 09The immediate-return side of the continuum. Catches `const x = compute(); return x;` directly. Auto-fixable. The third side of the rule, codified.
- 10Worked example with code samples. The teaching version of McConnell's §12.7. The named-constants side made operational.
- 11Python equivalent of the magic-numbers rule. The Pylint reference; Ruff inherits it under the same identifier.
- 12Modern Python surface for the same rule. Single binary, single TOML block. The right choice for new projects.
- 13Python equivalent of `prefer-immediate-return`. The same continuum, different surface.
- 14PMD: AvoidLiteralsInIfConditionSupportsJava surface for the named-constants side. Catches the magic-number-in-condition pattern that often surfaces during code review.
- 15“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.
- 16The 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.
Examples
Before a single-use alias that names nothing. sonarjs/prefer-immediate-return fires.
function formatTaxableHedgehogInvoice(invoice: HedgehogInvoice): string { const taxableAmount = computeBaseTax(invoice); return formatAmount(taxableAmount); }
After the binding is gone. The expression returns directly.
function formatTaxableHedgehogInvoice(invoice: HedgehogInvoice): string { return formatAmount(computeBaseTax(invoice)); }
Before a single-use alias that names nothing. sonarjs/prefer-immediate-return fires.
function formatTaxableHedgehogInvoice(invoice: HedgehogInvoice): string { const taxableAmount = computeBaseTax(invoice); return formatAmount(taxableAmount); }
After the binding is gone. The expression returns directly.
function formatTaxableHedgehogInvoice(invoice: HedgehogInvoice): string { return formatAmount(computeBaseTax(invoice)); }
Before a single-use alias that names nothing. sonarjs/prefer-immediate-return fires.
function formatTaxableHedgehogInvoice(invoice) { const taxableAmount = computeBaseTax(invoice); return formatAmount(taxableAmount); }
After the binding is gone. The expression returns directly.
function formatTaxableHedgehogInvoice(invoice) { return formatAmount(computeBaseTax(invoice)); }
Before a single-use alias that names nothing. PIE790 / RET504 fires.
def format_taxable_hedgehog_invoice(invoice: HedgehogInvoice) -> str: taxable_amount = compute_base_tax(invoice) return format_amount(taxable_amount)
After the binding is gone. The expression returns directly.
def format_taxable_hedgehog_invoice(invoice: HedgehogInvoice) -> str: return format_amount(compute_base_tax(invoice))
Before a single-use alias that names nothing. SonarQube prefer-immediate-return fires.
static String formatTaxableHedgehogInvoice(HedgehogInvoice invoice) { BigDecimal taxableAmount = computeBaseTax(invoice); return formatAmount(taxableAmount); }
After the binding is gone. The expression returns directly.
static String formatTaxableHedgehogInvoice(HedgehogInvoice invoice) { return formatAmount(computeBaseTax(invoice)); }
Before a single-use alias that names nothing. PHPStan / Psalm flag the redundant binding.
function formatTaxableHedgehogInvoice(HedgehogInvoice $invoice): string { $taxableAmount = computeBaseTax($invoice); return formatAmount($taxableAmount); }
After the binding is gone. The expression returns directly.
function formatTaxableHedgehogInvoice(HedgehogInvoice $invoice): string { return formatAmount(computeBaseTax($invoice)); }
Enforcement
Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| @typescript-eslint/no-magic-numbers | typescript-eslint | literals 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-return | eslint-plugin-sonarjs | the const x = compute(); return x; pattern. Auto-fixable: the alias drops and the function reads as the expression. |
| sonarjs/no-redundant-boolean | eslint-plugin-sonarjs | boolean literals used where the expression already evaluates to the same boolean. The shape that hides behind unnecessary intermediate bindings. |
| sonarjs/no-gratuitous-expressions | eslint-plugin-sonarjs | expressions 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',
}
});Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| @typescript-eslint/no-magic-numbers | typescript-eslint | literals 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-return | eslint-plugin-sonarjs | the const x = compute(); return x; pattern. Auto-fixable: the alias drops and the function reads as the expression. |
| sonarjs/no-redundant-boolean | eslint-plugin-sonarjs | boolean literals used where the expression already evaluates to the same boolean. The shape that hides behind unnecessary intermediate bindings. |
| sonarjs/no-gratuitous-expressions | eslint-plugin-sonarjs | expressions 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',
}
});Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| no-magic-numbers | ESLint core | literals that should be named constants. The same allowlist as TypeScript for 0, 1 and -1, array indexes and default values. |
| sonarjs/prefer-immediate-return | eslint-plugin-sonarjs | assign-then-return patterns. Auto-fixable. |
| sonarjs/no-redundant-boolean | eslint-plugin-sonarjs | redundant boolean literals. |
| no-useless-rename | ESLint core | renaming a destructured property to its own name. The trivial-binding case at the import / destructure level. |
eslint.config.mjsconfiguration snippet
import js from '@eslint/js';
import sonarjs from 'eslint-plugin-sonarjs';
export default [
js.configs.recommended,
{
files: ['**/*.{js,mjs,cjs}'],
plugins: { sonarjs },
rules: {
'no-magic-numbers': ['error', {
ignore: [-1, 0, 1],
ignoreArrayIndexes: true,
ignoreDefaultValues: true,
enforceConst: true,
}],
'sonarjs/prefer-immediate-return': 'error',
'sonarjs/no-redundant-boolean': 'error',
'no-useless-rename': 'error',
},
},
];Apply these rules in pyproject.toml. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| PLR2004 (magic-value-comparison) | Ruff (pylint) | literals used in comparisons where a named constant would carry intent. The named-constants side. |
| RET504 (unnecessary-assign) | Ruff (flake8-return) | value assigned to a name and then immediately returned. The Python equivalent of prefer-immediate-return; auto-fixable. |
| SIM103 (needless-bool) | Ruff (flake8-simplify) | if-true-else-false patterns that should return the boolean expression directly. The shape that hides behind unnecessary intermediate bindings. |
| SIM117 (multiple-with-statements) | Ruff (flake8-simplify) | nested with statements that could be combined. Removes a needless layer of binding scope. |
| R6103 (consider-using-assignment-expr) | Pylint | patterns where Python's walrus operator would inline a binding. The native Python tool for the inline-first instinct. |
pyproject.tomlconfiguration snippet
[tool.ruff.lint]
select = [
"PLR2004",
"RET504",
"SIM103",
"SIM117",
]
[tool.ruff.lint.pylint]
allow-magic-value-types = ["str", "bytes"]Apply these rules in pmd-ruleset.xml. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| AvoidLiteralsInIfCondition | PMD | literals used inside if conditions, other than -1, 0 and 1. The most-common surface for the named-constants conversation in Java, and the only one PMD has. |
| AvoidDuplicateLiterals | PMD | the same String literal repeated four or more times (maxDuplicateLiterals defaults to 4). Pairs with F3 Don't Repeat Yourself; the literal is the missing constant. |
| VariableCanBeInlined | PMD | the assign-then-return pattern in Java. Auto-fixable in most IDEs; the Java equivalent of prefer-immediate-return. |
| PrematureDeclaration | PMD | local variables declared before they are used. The Java surface for the inline-first instinct in long methods. |
| MagicNumber (Checkstyle) | Checkstyle | magic numbers anywhere in the code, not only in if conditions. The Java pack pairs it with PMD, with ignoreNumbers set to -1, 0 and 1. |
pmd-ruleset.xmlconfiguration snippet
<?xml version="1.0"?>
<ruleset name="InlineFirst"
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0">
<description>S5 Inline-First: magic numbers and unnecessary intermediate bindings.</description>
<rule ref="category/java/errorprone.xml/AvoidLiteralsInIfCondition">
<properties>
<property name="ignoreMagicNumbers" value="-1,0,1"/>
</properties>
</rule>
<rule ref="category/java/codestyle.xml/VariableCanBeInlined"/>
<rule ref="category/java/codestyle.xml/PrematureDeclaration"/>
<rule ref="category/java/errorprone.xml/AvoidDuplicateLiterals"/>
</ruleset>Apply these rules in composer.json. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| phpmnd | phpmnd (PHP Magic Number Detector) | numbers in conditions, returns, case labels, arguments, assignments, operations and property defaults, other than 0, 1 and -1. The PHP pack's run command leaves out its default_parameter and array checks, so default values and array indexes pass, as in the other packs. |
| SlevomatCodingStandard.Variables.UselessVariable | Slevomat Coding Standard | value assigned to a variable and then immediately returned. The PHP equivalent of prefer-immediate-return. Auto-fixable. |
| UnusedLocalVariable | PHPMD | local variables assigned but never used. Common signal of an intermediate binding that should have been inlined. |
| Generic.Files.LineLength | PHP_CodeSniffer | long lines that often hide unnecessary binding chains. Indirect surface for the inline-first conversation. |
composer.jsonconfiguration snippet
{
"scripts": {
"magic-numbers": "vendor/bin/phpmnd src --extensions=all,-default_parameter,-array --ignore-numbers=-1"
}
}AI rules
.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.
Counter-argument
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.
Counter-argument retort
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.
Notes
- [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]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]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.
- [4]Kent Beck: Smalltalk Best Practice Patterns (Prentice Hall, 1996). Beck argues that intermediate bindings are sometimes the cheapest way to document the meaning of an expression: the binding-as-documentation reading. The reply: pair Inline-First with F2.
- [5]Brian W. Kernighan & P. J. Plauger: The Elements of Programming Style (McGraw-Hill, 1974; 2nd ed. 1978). Rule 4: “Avoid too many temporary variables.” An early published statement of the inline side of the continuum.
- [6]Martin Fowler: Refactoring catalogue: “Inline Variable” (refactoring.com). The operational form of the move: replace the binding with the expression at every use. Remove the binding declaration. Run the tests.