I would start with walkAST in packages/pug-walk/index.js. Its cyclomatic complexity is 73, against 14 for the next-highest function on my list, and that 59-point gap puts it in a different category from everything else in the top five. It is structural debt, not live churn: it hasn’t been changed in 2141 days, so the case for refactoring is the high blast radius when someone next has to touch it, not an active regression risk. In this snapshot of pug (96 functions analyzed, 12 in the critical band), 33 functions sit in the debt quadrant and exactly one, Compiler, is both complex and actively changing.
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 |
|---|---|---|---|---|---|
walkAST | packages/pug-walk/index.js | 14.6 | 73 | 3 | 9 |
compileAttrs | packages/pug-attrs/index.js | 11.9 | 14 | 3 | 23 |
handleFilters | packages/pug-filters/lib/handle-filters.js | 11.4 | 8 | 2 | 15 |
pug_escape | packages/pug-runtime/index.js | 10.9 | 14 | 2 | 3 |
compileBody | packages/pug/lib/index.js | 10.9 | 8 | 1 | 22 |
96 functions analyzed
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.Long Function×4Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.God Function×3God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.Stale Complex×2Stale Complex
High structural complexity but untouched for a long time — structural debt that will bite whoever opens it next.
The quadrant split was the first thing I noticed. Of 96 functions, 62 are low-complexity and quiet, 33 are complex and untouched, and one is complex and changing right now. All five functions in my top list are in the debt quadrant, each with zero touches in the last year. That changes how I read the list: these are not places where regressions are being introduced this week. They are places where the next change will be expensive, because the code is complicated and nobody has recent context on it. Every one of the five has zero distinct authors in the last 90 days.
walkAST — pug-walk/index.js
walkAST is the traversal engine for Pug’s syntax tree, and the source excerpt shows where the complexity comes from: a large switch over node types (Block, Case, Filter, Mixin, Tag, Each, Conditional, Include, Extends, RawInclude and more), each branch with its own conditional handling of child blocks, alternates, or files. Nesting depth is only 3 and fan-out is 9, so the problem is not tangled control flow or broad coupling. It is breadth: 73 independent paths through one function, each a potential test case. It is also flagged as exit-heavy and long, and it has not been changed in 2141 days.
The history is thin. The file has 3 commits in total, a bug-fix rate of 33%, no reverts, and no bug-linked commits. I would not read that as a defect signal; with so few commits it mostly says the file has rarely been edited. What it does tell me is that very little review history exists for a function this central. The excerpt also shows a recursive structure where each case calls back into the walker, so a mistake in one branch can propagate through every node type that nests it.
I would refactor to a dispatch table. Move each node-type case into its own small handler keyed by node type, keep walkAST as the thin loop that handles the before/after callbacks and parent stack, and write one test per handler before moving any logic. That should cut the cyclomatic complexity of the main function by well over half and make each handler testable on its own. Because the function has been stable for years, characterization tests that pin current behavior are the safe first step.
compileAttrs — pug-attrs/index.js
Here the story is coupling, not branching. Cyclomatic complexity is a moderate 14, but fan-out is 23, the highest in my top five, and the function is flagged as both a god function and long. From the excerpt, it opens with a block of input assertions on the attribute array and options, defines a nested addAttribute helper that branches on whether a value is constant and whether the output format is ‘html’ or ‘object’, and then iterates over the attributes with special handling for class. Validation, constant folding, string assembly, and attribute-specific rules all share one function.
It hasn’t been changed in 2141 days. The file has 2 commits, half tagged as bug fixes (a bug-fix rate of 50%), with no reverts. Two commits is too small a sample to conclude anything, so I treat this as maintainability debt. With a fan-out of 23, a change to any of those callees can alter attribute output, and because the output is generated code and markup strings, mistakes may surface far from the cause.
I would extract the input validation into its own function first, since it is mechanical and low-risk, then pull addAttribute out of the closure so it can be tested with explicit arguments. That alone should remove a visible chunk of both length and fan-out.
handleFilters — pug-filters/lib/handle-filters.js
This one has a modest cyclomatic complexity of 8 and nesting depth of 2, but fan-out is 15 and it is flagged as a god function, exit-heavy, and long. The source excerpt shows why: it walks the tree with a callback that handles two node shapes (a Filter node and a RawInclude node with filters), and defines several inner functions (filterWithFallback, filterFileWithFallback, and getFilterName with alias-chain detection) inside it. Much of the logic lives in closures inside the function.
It hasn’t been changed in 2123 days. The file has 5 commits, a bug-fix rate of 20%, no reverts, and no bug-linked commits, so the history is unremarkable. I would call this testability debt: the interesting behavior (fallback from custom filters to the registry, alias resolution, error translation for unknown filters) is buried in nested functions.
I would hoist the inner helpers to module level with explicit parameters instead of closing over dir, filters, and filterAliases. That turns three untestable closures into three directly testable functions and leaves handleFilters as orchestration.
pug_escape — pug-runtime/index.js
pug_escape is the odd one on the list, and I would handle it differently. Fan-out is 3 and nesting is 2; the complexity of 14 comes from a character-code switch that maps four characters to HTML entities, plus a pre-check regex and a tail-handling branch. From the excerpt, it scans the string and copies unchanged runs, appending only when it hits a character that needs escaping. That is a hand-tuned loop, a common shape for code on a rendering hot path.
It hasn’t been changed in 2141 days. The file has 6 commits, a bug-fix rate of 33%, and a pull-request review comment density of 1.0, one of only two top-five files with any recorded review discussion. Since this function escapes output, correctness matters more than its metrics suggest, but I would not restructure it for the sake of the numbers. The right move is a thorough test table: each of the four special characters at the start, middle, and end of a string, strings with no special characters, empty strings, and non-string inputs. If those tests exist, I would leave the function alone and treat the score as a flag for test coverage, not for refactoring.
compileBody — pug/lib/index.js
compileBody is the main compile pipeline, and its profile is fan-out 22 with a cyclomatic complexity of only 8 and nesting depth of 1. It is flagged as a god function, exit-heavy, and long. The excerpt shows it building an AST through load.string with inline lex, parse, resolve, and read callbacks, each of which copies options objects, filters plugins, and applies plugin hooks at stages named preLex, postLex, preParse, postParse, and preLoad. The structure is flat, but the whole pipeline is wired together in one function body.
It hasn’t been changed in 1838 days, the most recently touched function in my top five, though still more than five years ago. The file has 7 commits, a bug-fix rate of 43%, no reverts, and a pull-request review comment density of 3.0, the highest of the five. That is the one signal on this list I would take seriously as context: this file has drawn more review discussion than the others, which suggests the pipeline wiring is a place where people have had opinions. It is not evidence of defects.
I would extract each inline callback (lex, parse, resolve, read) into a named function that takes the plugin list and options explicitly. The repeated pattern of copying option keys and filtering plugins by capability appears in both the lex and parse paths and is an obvious extraction candidate.
What I would do first
The five functions here need different treatment: walkAST needs decomposition, compileAttrs and compileBody need their fan-out broken up, handleFilters needs its closures hoisted, and pug_escape needs tests more than restructuring. None of them is under active change, which makes this the best time to refactor: behavior-preserving changes can be reviewed without competing with feature work.
Patterns Found
Antipatterns detected across the top functions in this snapshot:
| Pattern | Occurrences |
|---|---|
exit_heavy | 4 |
long_function | 4 |
god_function | 3 |
stale_complex | 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.
See more analyses with these patterns: exit_heavy, god_function, long_function, stale_complex.
Reproduce This Analysis
git clone https://github.com/pugjs/pug
cd pug
git checkout c323ed3e630b931bc04790efb509d34ac5927040
hotspots analyze . --mode snapshot --explain-patterns --force
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 →