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
| Function | File | Risk | CC | ND | FO |
|---|---|---|---|---|---|
print | src/language-handlebars/printer-glimmer.js | 26.1 | 112 | 5 | 54 |
needsParentheses | src/language-js/parentheses/needs-parentheses.js | 19.5 | 572 | 4 | 29 |
printDocToString | src/document/printer/printer.js | 18.9 | 65 | 6 | 20 |
printCommaSeparatedValueGroup | src/language-css/print/comma-separated-value-group.js | 18.8 | 187 | 5 | 51 |
embed | src/language-html/embed.js | 18.7 | 27 | 5 | 19 |
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
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.
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 — printer-glimmer.js
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 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.
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 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 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.
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 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:
| Pattern | Occurrences |
|---|---|
complex_branching | 5 |
exit_heavy | 5 |
god_function | 5 |
long_function | 5 |
deeply_nested | 4 |
cyclic_hub | 2 |
hub_function | 2 |
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 →