prettier/prettier's printer layer carries the highest activity risk — 5 functions to address first

Five critical-band functions spanning prettier's Handlebars, JavaScript, CSS, HTML, and core document-printer layers are all in the 'fire' quadrant — structurally complex and actively changing as of commit cf7db35.

Stephen Collins ·
Generated by hotspots · free & open source
Activity Risk26.08Low
Hottest Functionprint

Antipatterns Detected

complex_branching5exit_heavy5god_function5long_function5deeply_nested4cyclic_hub2hub_function2

Run this on your own codebase

Hotspots runs locally in under a minute — no account, no data leaves your machine.

pip
$ pip install hotspots-cli
npm
$ npm install -g @stephencollinstech/hotspots
Run in any repo
$ hotspots analyze .
★ Star on GitHub

Key Points

What is a god function and why does it matter in prettier?

A god function is a single function that owns too many responsibilities — it handles so many cases, maintains so much state, and calls so many other functions that it becomes the load-bearing center of a subsystem. In prettier, all five top hotspots are flagged as god functions: `needsParentheses` makes 29 direct calls and contains 572 execution paths, while `print` in the Handlebars printer calls 54 distinct functions. The practical problem is that a god function concentrates blast radius: any change to it can affect every code path it contains, making safe modification difficult and meaningful test coverage expensive. It also creates an implicit ownership bottleneck — the function becomes the only place to add new cases, so every new language feature or edge case accretes there rather than in a focused module.

How do I reduce cyclomatic complexity in JavaScript?

The most effective technique is extract-method decomposition: identify logically cohesive groups of branches and pull them into named functions, each testable in isolation. A cyclomatic complexity above 15 warrants splitting; above 30 it should be treated as a blocker on new functionality being added to the same function. For `needsParentheses`, with a CC of 572, I would start by grouping the switch cases by AST node category — binary expressions, update expressions, assignment expressions — and extracting each group into a `needsParenthesesForBinaryExpression(path, options)` style function. Each extracted function reduces the CC of the dispatcher by the number of branches it absorbs, and immediately becomes independently unit-testable.

Is prettier actively maintained?

Yes, and the commit data makes that concrete: all five critical-band functions are in the fire quadrant, meaning they are both structurally complex and receiving commits right now. `printCommaSeparatedValueGroup` was touched twice in the last 30 days and had a change land today (days_since_changed: 0); `needsParentheses`, `print`, `printDocToString`, and `embed` were each touched once in the last 30 days and last changed 3 days ago. Active maintenance and structural complexity are not mutually exclusive — in fact, the fire quadrant is precisely the combination that warrants attention, because engineers are actively modifying code that carries the highest structural risk.

How do I reproduce this analysis?

The analysis was run against commit `cf7db35` of `prettier/prettier` using the Hotspots CLI, available at https://github.com/hotspots-dev/hotspots. After checking out the repo at that commit with `git checkout cf7db35`, run `hotspots analyze . --mode snapshot --explain-patterns --force` from the repo root. The same command works on any local git repository without additional configuration.

What does activity-weighted risk mean?

Activity-weighted risk (shown as `activity_risk` in the data) multiplies a function's structural complexity — derived from cyclomatic complexity, nesting depth, and fan-out — by a signal reflecting how frequently recent commits have touched it. A function with very high structural complexity that has not been changed in years scores lower than one with moderate complexity that is being modified every few days, because the dormant function's next change is not imminent. This prioritization is designed to surface where a bug is most likely to be introduced right now, not just where the code is hardest to read in the abstract. In prettier's case, the fact that all five top-ranked functions are in the fire quadrant — complex and recently active — means these are live regression risks for any engineer merging a PR this week. The top score in this analysis is 26.08, belonging to `print` in `src/language-handlebars/printer-glimmer.js`.

At commit cf7db35, prettier carries 194 critical-band functions out of 3,609 total, and every one of the top five hotspots sits in the fire quadrant — high structural complexity combined with active recent commits. The top-ranked function by activity-weighted risk is print in the Handlebars printer (26.08), but the structural outlier I want to flag immediately is needsParentheses in src/language-js/parentheses/needs-parentheses.js: a cyclomatic complexity of 572 is extreme by any standard, and it was touched 3 days ago. The most time-sensitive target is printCommaSeparatedValueGroup in the CSS layer, which had 2 touches in the last 30 days and was modified as recently as today.

The table below ranks functions by activity-weighted risk — a score that multiplies structural complexity by recent commit frequency. A function that is both hard to understand (high cyclomatic complexity) and actively changing is a higher priority than one that is complex but untouched. CC = cyclomatic complexity (independent execution paths); ND = max nesting depth; FO = fan-out (distinct callees).

Top 5 Hotspots

FunctionFileRiskCCNDFO
printsrc/language-handlebars/printer-glimmer.js26.1112554
needsParenthesessrc/language-js/parentheses/needs-parentheses.js19.5572429
printDocToStringsrc/document/printer/printer.js18.965620
printCommaSeparatedValueGroupsrc/language-css/print/comma-separated-value-group.js18.8187551
embedsrc/language-html/embed.js18.727519

Large Repo Analysis

prettier is a large repository. To stay within memory constraints, this analysis used hybrid touch mode: structural complexity — CC, ND, FO — is measured precisely for every function. Git activity is tracked at the function level (via git log -L) only for files with 5 or more commits in the last 30 days; other files use a file-level approximation. Rankings therefore surface functions that are both structurally complex and in the most actively-changing parts of the codebase. Dormant code with high structural complexity will rank lower than it would under a full per-function analysis — to surface it, run hotspots analyze . --per-function-touches on a machine with sufficient memory.

Quadrant and pattern overview

Triage Band Distribution
Fire583Watch3026

3,609 functions analyzed

Every function in this repo falls into either fire or watch — there are no debt-quadrant or ok-quadrant functions at all. That means structural complexity and recent activity are correlated throughout the codebase: the complex functions are also the ones being actively changed. For a formatter that touches every language it supports on every save, this is structurally predictable — but it raises the stakes on any individual commit to these files.

Detected Antipatterns
Complex Branching×5Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.
Exit Heavy×5Exit Heavy
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.
God Function×5God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.
Long Function×5Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.
Deeply Nested×4Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.
Cyclic Hub×2Cyclic Hub
Participates in a call cycle with other high-traffic functions, creating circular dependency risk.
Hub Function×2Hub Function
Many other functions call this one — a change here ripples widely through callers.

Every one of the top five functions is flagged as a god function, a long function, complex branching, and exit-heavy. Four of five are also deeply nested. Two carry the cyclic hub and hub function patterns, which I’ll address when they appear. None of the external signals show historical bug-linked commits or reverts on these files — the risk here is structural and forward-looking, not a record of past defects.


print
src/language-handlebars/printer-glimmer.js
26.08
critical
CC 112
ND 5
FO 54
touches/30d 1

print in the Glimmer/Handlebars printer is the top-ranked function by activity-weighted risk at 26.08. From the source excerpt it is a large switch dispatch over every Handlebars AST node type — Block, Program, Template, ElementNode, BlockStatement, MustacheStatement, SubExpression, and more. Every case branch is effectively a separate formatting rule, and there are 112 independent execution paths through it. Max nesting depth reaches 5, driven by the ElementNode case alone which branches on whitespace sensitivity, void elements, empty children, and style tags before it settles on a layout. Fan-out of 54 means this single function directly invokes 54 other functions — printStartingTag, printOpenBlock, printCloseBlock, printProgram, printInverse, printElseIfBlock, printPathAndParams, and many more. In JavaScript, where dynamic dispatch can hide additional call edges, the real coupling surface is likely wider than what static analysis captures.

The god function and cyclic hub patterns are the key concern here. A change to handle a new Handlebars AST node type, or to adjust whitespace behavior for ElementNode, must be made inside a function that already coordinates 54 callees. The exit-heavy pattern (many early returns scattered across cases) means test coverage requires exercising every branch combination independently — with CC 112, that is at minimum 112 distinct test paths.

This function was last changed 3 days ago, making it a live regression surface.

Recommendation: Extract each major switch case into a dedicated printBlock, printElement, printMustache, etc. function. The ElementNode case alone is complex enough to warrant its own module. The print dispatcher should shrink to a thin router. The companion functions printProgram, printParams, and printOpenBlock in the same file (visible in the context data, all in the watch quadrant) are already reasonably contained — that decomposition pattern is the right model to apply to the main print function.


needsParentheses — needs-parentheses.js

needsParentheses
src/language-js/parentheses/needs-parentheses.js
19.54
critical
CC 572
ND 4
FO 29
touches/30d 1

needsParentheses has a cyclomatic complexity of 572 — the highest by a wide margin in this analysis and one of the more extreme values I encounter in production codebases.

Cyclomatic Complexity 572
threshold: 30

From the source excerpt, the function opens with several fast-exit guard clauses — returning false for root nodes, returning early for statement nodes, delegating identifiers to shouldAddParenthesesToIdentifier — and then enters a large switch over node.type that handles UpdateExpression, ObjectExpression, FunctionExpression, ClassExpression, DoExpression, and many more AST node types. Each case encodes JavaScript operator precedence and grammar rules about when parentheses are required to preserve semantics. The ObjectExpression case alone has two separate ancestor-lookup branches: one for expression statement context and one for arrow function body context.

A cyclomatic complexity of 572 means there are 572 linearly independent paths through this function. Each path represents a case where prettier must decide whether to emit parentheses. JavaScript’s grammar is legitimately complex in this domain — operator precedence, destructuring contexts, async/await, optional chaining, and newer syntax like DoExpression all interact — but concentrating all of those decisions in one function makes it very hard to reason about any individual rule in isolation, and nearly impossible to achieve thorough test coverage. The exit-heavy pattern amplifies this: there are many early-return exits before the main switch, so the effective entry condition for each case is a conjunction of all the preceding guards.

Fan-out of 29, combined with JavaScript’s dynamic property access patterns, means parentNeedsParentheses and other helpers called here may themselves carry hidden coupling back into this function.

This file was touched 1 time in the last 30 days and last changed 3 days ago.

Recommendation: Decompose by node type category. Each major AST node type — or at minimum each operator-precedence group — should have its own needsParenthesesFor<NodeType> function, each testable in isolation. The existing fast-exit guards at the top of the function are a good pattern; extend that approach so the main function becomes a dispatcher rather than an oracle. A CC above 30 warrants splitting; at 572 this is immediate.


printDocToString — printer.js

printDocToString
src/document/printer/printer.js
18.9
critical
CC 65
ND 6
FO 20
touches/30d 1

printDocToString is prettier’s core document-to-string renderer. The source excerpt shows it initializes a stack-based command loop — deliberately iterative rather than recursive, per an inline comment noting the performance benefit — and then dispatches over document node types: DOC_TYPE_STRING, DOC_TYPE_ARRAY, DOC_TYPE_INDENT, DOC_TYPE_ALIGN, DOC_TYPE_TRIM, DOC_TYPE_GROUP, and more. The DOC_TYPE_GROUP case itself contains an inline printGroup IIFE that checks flat/break mode and remaining line width, adding another layer of branching inside an already nested structure.

Max nesting depth of 6 is the highest in the top five, driven by the combination of the outer while loop, the switch, the DOC_TYPE_GROUP case, the printGroup IIFE, and inner conditionals. A nesting depth of 6 means a developer reading any inner branch must hold six levels of context simultaneously. The long function and god function patterns reflect that this single function owns mode tracking, position accounting, line-suffix buffering, group-mode mapping, and the break-propagation side effect (propagateBreaks(doc) is called before the loop).

Cyclomatic complexity of 65 with nesting depth of 6 places this squarely in territory where any new document node type added to the IR requires careful surgery inside a function that is already managing a lot of state. It was last touched 3 days ago.

Recommendation: The DOC_TYPE_GROUP case with its inline printGroup IIFE is a natural extraction point — pulling that into a named printGroupCommand function would reduce both the nesting depth and the CC of the outer loop. The broader opportunity is to make each switch case a standalone handler that accepts and mutates the loop’s shared state explicitly, similar to how the iterative stack was introduced to replace recursion.


printCommaSeparatedValueGroup — comma-separated-value-group.js

printCommaSeparatedValueGroup
src/language-css/print/comma-separated-value-group.js
18.76
critical
CC 187
ND 5
FO 51
touches/30d 2

printCommaSeparatedValueGroup is the most actively changed function in this set: 2 touches in the last 30 days, modified as recently as today. It also has the highest review-comment rate among the top five — roughly 1.2 comments per PR — suggesting it has drawn consistent reviewer attention, and it has had 2 distinct authors active in the last 90 days.

Cyclomatic Complexity 187
threshold: 30

From the source excerpt, the function handles CSS comma-separated value groups — grid template values, SCSS control directives, URL functions, SCSS @forward wildcards, Tailwind @utility directives, and inline comments — all within a single iterating loop over node.groups. The loop accumulates parts using an unusual invariant (parts always has odd length; even-indexed elements are line-like) and then branches on node type, context, and predecessor/successor node relationships for each element. Variables like insideSCSSInterpolationInString, insideURLFunction, and didBreak are maintained as mutable flags across loop iterations.

A cyclomatic complexity of 187 with fan-out of 51 means this function both makes 51 direct calls and contains 187 decision points — the combination of high branching and high coupling is what pushes it into the critical band. The mutable-flag pattern across a complex loop is a particular test-coverage burden: the behavior at any iteration depends on accumulated state from all prior iterations, which multiplies the number of scenarios that need to be exercised.

Given that it was changed today and has drawn previous review comments, I would treat this as the most time-sensitive refactoring target in the set.

Recommendation: Start by extracting the per-node-type handling into separate functions — printGridValueGroup, printUrlFunctionGroup, printSCSSForwardGroup — so each special case can be tested independently. The mutable accumulation loop itself should be the last thing to refactor, not the first, because it is the structural glue holding the extractions together. The existing TODO comment visible in the source excerpt (// TODO: Improve this part) signals that the maintainers already recognize a specific subproblem worth addressing.


embed — embed.js

embed
src/language-html/embed.js
18.67
critical
CC 27
ND 5
FO 19
touches/30d 1

embed in the HTML printer handles the embedding of sub-languages inside HTML — Vue single-file component blocks, <script> and <style> tags, inline JavaScript expressions in interpolation contexts. The source excerpt shows a switch over node.kind with cases for element and text, each returning async closures that call textToDoc to recursively format embedded content with a different parser. The element case checks for Vue non-HTML blocks, infers the parser, strips and re-indents content, and assembles opening tag, content, and closing tag. The text case branches over script-like parents, markdown, Babel with module/script source type detection, and interpolation contexts.

Cyclomatic complexity of 27 is moderate by general standards but worth noting because it is concentrated inside async closures returned from within a switch — meaning the branching happens at two levels: which case is entered, and then what the returned async function does when invoked. Nesting depth of 5 reflects this two-level structure. Fan-out of 19 spans HTML printing utilities, the textToDoc callback, whitespace handling, and tag utilities.

The deeply nested and exit-heavy patterns are visible in the early-return guards (if (!parser) { return; }, the return; for script-like tags in the element case) before the function commits to returning an async closure. This structure means the return type of embed is effectively undefined | AsyncFunction, and every caller must handle both cases.

Recommendation: The source type detection logic inside the text case — determining whether a <script> block is "module" or "script" by inspecting attrMap — is a self-contained concern that could be extracted into an inferScriptSourceType(node) utility. That would reduce both CC and ND for the embed function itself, and make the source-type logic independently testable.


What this means for contributors

All 583 fire-quadrant functions in prettier are structurally complex and recently active. The five analyzed here are the highest priority because they combine the largest structural footprints with commits landing this week. The absence of bug-linked commits or reverts in the external signals for all five files is genuinely reassuring — this is structural risk that has been managed carefully, not code that has been visibly failing. But structural complexity does not need a history of bugs to create future ones: it creates them when the next engineer, under time pressure, makes a change inside a 572-path function without being able to reason about all 572 paths.

Patterns Found

Antipatterns detected across the top functions in this snapshot:

PatternOccurrences
complex_branching5
exit_heavy5
god_function5
long_function5
deeply_nested4
cyclic_hub2
hub_function2

These labels belong to two tiers — Tier 1 (structural): complex_branching, deeply_nested, exit_heavy, long_function, god_function. Tier 2 (relational/temporal): hub_function, cyclic_hub, middle_man, neighbor_risk, stale_complex, churn_magnet, shotgun_target, volatile_god.

Reproduce This Analysis

git clone https://github.com/prettier/prettier
cd prettier
git checkout cf7db3500a89faeb24ad0af45c6b9a0e7b074a03
hotspots analyze . --mode snapshot --explain-patterns --force --hybrid-touches 5

To run the same analysis on your own codebase, run hotspots analyze . --mode snapshot in any local git repo — no configuration required.

I use Hotspots to highlight structural and activity risk — not “bad code.” I treat these findings as a prioritization aid, not a bug predictor. Editorial policy →

Related Analyses