Summary
During a recent refactor of a large-scale JavaScript codebase, our CI/CD pipeline flagged inconsistent styling patterns. Specifically, multiple function declarations were being collapsed onto a single line without a separator, creating significant readability debt and increasing the cognitive load during code reviews. While the team expected the automated formatter (Prettier/VSCode) to enforce a newline between function blocks, it failed to do so, treating them as distinct but adjacent statements.
Root Cause
The issue stems from a misunderstanding of how AST (Abstract Syntax Tree) parsers and formatters interpret statement boundaries.
- Statement vs. Block Structure: Formatters like Prettier primarily focus on statement separation. In JavaScript, two function declarations are technically two separate statements.
- Implicit vs. Explicit Rules: Most default formatting configurations prioritize reducing vertical whitespace unless a specific rule dictates a “gap” between certain node types.
- The “If-Else” Exception: The reason
if/elseblocks behave differently is thatelseis a keyword that syntactically attaches to the precedingifblock, creating a single Control Flow Statement. Function declarations, however, are independent entities with no structural link.
Why This Happens in Real Systems
In massive production repositories, formatting is rarely governed by “gut feeling” and is instead governed by deterministic rulesets.
- Rule Ambiguity: Standard formatters often lack a specific rule for “maximum vertical whitespace between declarations.”
- Parser Logic: The formatter sees
FunctionDeclaration->FunctionDeclaration. Without a rule that saysif (node == FunctionDeclaration) add_newline(), the engine optimizes for density. - Config Drift: Different developers may have different local VSCode settings (like
formatOnSave) that conflict with the project’s.prettierrcor.eslintrc, leading to “formatting wars” in Pull Requests.
Real-World Impact
- Reduced Scannability: Engineers spend more time “parsing” code visually to find where one function ends and another begins.
- Merge Conflict Bloat: If one developer manually adds a newline and another’s formatter removes it, every subsequent touch to that file generates unnecessary git diffs.
- Code Review Friction: Senior engineers spend time correcting whitespace instead of reviewing logic and architectural integrity.
Example or Code
// Current problematic state
function initializeService() {
console.log("Service started");
} function shutdownService() {
console.log("Service stopped");
}
// Desired state after correct configuration
function initializeService() {
console.log("Service started");
}
function shutdownService() {
console.log("Service stopped");
}
How Senior Engineers Fix It
Senior engineers do not solve this by manually pressing “Enter.” They solve it through systemic enforcement.
- Configuration Overhaul: Instead of fighting the formatter, we define a strict Prettier or ESLint configuration that enforces
newline-per-functionlogic if the tool supports it. - Custom ESLint Rules: If the standard formatter is too opinionated, we implement a custom rule using
eslint-plugin-styleor a regex-based lint rule to mandate a blank line betweenFunctionDeclarationnodes. - Pre-commit Hooks: We use Husky and lint-staged to ensure that no code can be committed unless it satisfies the newline requirements, making the fix invisible to the daily workflow.
Why Juniors Miss It
- Focus on Syntax over Semantics: Juniors often view the problem as a “VSCode glitch” rather than a parser configuration issue.
- Manual Correction Habit: A junior will often manually add the newline, only to have the formatter strip it away seconds later, leading to frustration and wasted time.
- Missing the “Why”: They may not realize that the
if/elsebehavior is due to language grammar (theelsebelonging to theif), leading to incorrect assumptions about how the formatter views “blocks.”