Skip to content

Require Readable Spacing

NewSuggestionAuto-fixable
small-rules/require-readable-spacing

Require readable spacing between declarations and logical statement groups.

Rationale

Ported from dmmulroy/anti-slop, which builds it on padding-line-between-statements from ESLint Stylistic. Generated code often packs imports, declarations, and control flow into one block of text. This rule requires a blank line after imports and between top-level statements. It also requires one around functions, classes, interfaces, types, and bindings that span lines. Control flow gets one before it, and a statement that ends in a block gets one after it. Runs of one-line bindings stay grouped, and so do imports and overloads. A binding with its own comment starts a new group. The fix adds blank lines and never removes them.

With no options, the rule follows anti-slop. Spacing is a matter of taste, so every part of that policy can change. Each requirement has its own switch:

  • afterImports: a blank line after the import group.
  • betweenTopLevelStatements: blank lines between top-level statements.
  • aroundDeclarations: blank lines around functions, classes, interfaces, and types.
  • aroundMultilineBindings: blank lines around const, let, and var that span lines.
  • beforeControlFlow: a blank line before if, loops, switch, try, and return.
  • afterBlocks: a blank line after a statement that ends in a block, such as an if or a function.

The allow switches, off by default, keep related statements together:

  • allowAttachedUses: below module scope, a statement may follow a one-line statement with no blank line when it reads a name that statement declares or assigns. That covers const ready = await check(path); followed by if (ready) {, let result = ""; before the loop that builds result, and index = next(index); before if (index === -1) return;.

  • allowGuardClauseRuns: one-line if statements stay grouped, and so does a one-line return or throw right after one. A multi-line if still needs a blank line after it.

  • allowMultilineBindingRuns: consecutive bindings stay grouped even when one of them spans lines. A binding with its own comment still starts a new group.

For anything the switches don’t cover, overrides takes entries shaped like ESLint Stylistic’s: { blankLine, previous, next }. The rule checks them in order, before anything built in, and the first entry that matches a pair of adjacent statements decides it. blankLine: "always" requires a blank line and blankLine: "any" accepts either spacing. previous and next each take one statement kind or a list of them. Add usesPrevious: true to match only when the second statement reads a name the first one declares or assigns. For example, this keeps a one-line binding next to the function below it: { blankLine: "any", previous: "single-line-binding", next: "function" }. Turn every requirement switch off and overrides becomes a policy of your own.

The statement kinds are:

  • *: any statement.
  • binding, single-line-binding, multiline-binding: const, let, var, and using declarations.

  • declaration, function, class, interface, type, enum, overload: declarations, where overload is a function signature with no body.

  • import, export, and top-level, which excludes imports.
  • expression: a call, an assignment, or another expression on its own.
  • control-flow, if, single-line-if, loop, switch, try, return, throw: control flow, where control-flow covers if, loops, switch, try, and return.

  • block-like: a statement that ends in a block, such as an if, a function, or const f = () => {};.
  • single-line, multiline: any statement, by whether it fits on one line.

Declaration and binding kinds match through export and export default, so function matches export function detect() {} too.

This port differs from anti-slop in a few ways:

  • All the options are local, since upstream has none.
  • It matches functions, classes, interfaces, types, and bindings by node type, so abstract, declare, and decorated forms count too. Exported bindings count as bindings, including multi-line ones.

  • It checks export = value like any other statement. Upstream skips it.
  • It keeps only the parts of ESLint Stylistic’s engine that this rule uses. The rule never removes blank lines.

Diagnostic Messages

expectedBlankLine
Expected blank line before this statement.

Configuration

This rule accepts one options object after the severity.

afterBlocksOptional

Require a blank line after a statement that ends in a block

afterImportsOptional

Require a blank line after the import group

allowAttachedUsesOptional

Let a statement stay attached to the one-line statement above it when it reads a name that statement declares or assigns

allowGuardClauseRunsOptional

Let one-line `if` statements, and a one-line `return` or `throw` after one, stay grouped

allowMultilineBindingRunsOptional

Let consecutive bindings stay grouped when one of them spans lines

aroundDeclarationsOptional

Require blank lines around functions, classes, interfaces, and types

aroundMultilineBindingsOptional

Require blank lines around bindings that span lines

beforeControlFlowOptional

Require a blank line before control flow statements

betweenTopLevelStatementsOptional

Require blank lines between top-level statements

overridesOptional

Padding entries checked before the built-in ones, highest priority first. The first entry that matches a pair of statements decides it

{
"jsPlugins": [
"@pobammer-ts/small-rules"
],
"rules": {
"small-rules/require-readable-spacing": [
"error",
{
"afterBlocks": true,
"afterImports": true,
"allowAttachedUses": false,
"allowGuardClauseRuns": false,
"allowMultilineBindingRuns": false,
"aroundDeclarations": true,
"aroundMultilineBindings": true,
"beforeControlFlow": true,
"betweenTopLevelStatements": true,
"overrides": []
}
]
}
}

Examples

cramped statement groups
import { readFile } from "node:fs/promises";
export async function readConfig(path: string) {
const text = await readFile(path, "utf8");
const config = JSON.parse(text);
return config;
}

After auto-fix

import { readFile } from "node:fs/promises";
export async function readConfig(path: string) {
const text = await readFile(path, "utf8");
const config = JSON.parse(text);
return config;
}
grouped bindings, separated logic
import { readFile } from "node:fs/promises";
export async function readConfig(path: string) {
const text = await readFile(path, "utf8");
const config = JSON.parse(text);
return config;
}