Across the 36 functions I analyzed in yarnpkg/yarn at commit c2dda50, 4 came back critical — and all 4 sit in src/util/generate-pnp-map-api.tpl.js, the template that generates yarn’s Plug’n’Play runtime resolver. Every one of those functions is in the debt quadrant: structurally complex but untouched for 2,167 days, carrying activity-weighted risk scores between 11.44 and 14.68. I would start with resolveToUnqualified, which tops the list at 14.68, because its combination of cyclomatic complexity 24, fan-out 20, and six years of dormancy means anyone reopening that file inherits significant blast-radius risk with no recent institutional memory to lean on.
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 |
|---|---|---|---|---|---|
resolveToUnqualified | src/util/generate-pnp-map-api.tpl.js | 14.7 | 24 | 4 | 20 |
applyNodeExtensionResolution | src/util/generate-pnp-map-api.tpl.js | 14.6 | 26 | 4 | 14 |
resolveRequest | src/util/generate-pnp-map-api.tpl.js | 13.6 | 20 | 4 | 7 |
setup | src/util/generate-pnp-map-api.tpl.js | 11.4 | 7 | 2 | 20 |
getCallee | scripts/eslint-rules/warn-language.js | 6.6 | 11 | 1 | — |
Large Repo Analysis
yarn 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.
The story is really about one file
Before looking at individual functions, the file-level pattern deserves attention: 4 of the 5 highest-risk functions in this repository are co-located in src/util/generate-pnp-map-api.tpl.js. That is not a coincidence — it reflects how the PnP API was originally authored as a self-contained runtime shim, with resolution logic packed densely into a small number of large functions. The .tpl.js extension signals that this file is a template, meaning its output is generated code injected into projects at install time. That adds an extra layer of caution: a refactor here touches not just the source but every generated PnP runtime downstream.
36 functions analyzed
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.Complex Branching×3Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.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.Long Function×3Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.Stale Complex×3Stale Complex
High structural complexity but untouched for a long time — structural debt that will bite whoever opens it next.
With zero functions in the fire quadrant and 31 in the ok band, this is not a codebase under active regression pressure — it is a codebase carrying concentrated structural debt in one corner that will become urgent the moment that corner is reopened.
resolveToUnqualified — generate-pnp-map-api.tpl.js
This function handles the first half of PnP module resolution: given a request string and an issuer path, it figures out the unqualified filesystem path for the requested package. From the source excerpt, the logic has to handle at least six distinct cases before returning: the reserved pnpapi request, native builtins, issuer paths that match an ignore pattern (falling back to Node’s native resolver), absolute paths, relative paths, and full package-name lookups that require traversing the PnP dependency map.
That branching structure is what drives a cyclomatic complexity of 24. Each of those paths is an independent execution branch, and each one is a required test case to cover fully. The fan-out of 20 means the function reaches out to 20 distinct callees — callNativeResolution, makeError, findPackageLocator, getPackageInformationSafe, normalizePath, and others — making it a hub whose behavior depends on a wide surface of collaborators. In JavaScript, where some of those calls may go through dynamic property access or prototype dispatch, the static fan-out count likely undercounts the true coupling.
The god_function and long_function patterns confirm what the metrics suggest: this function is doing too much. The exit_heavy pattern adds a test-coverage burden on top — multiple early-return paths mean a test suite needs to exercise each exit independently to achieve meaningful coverage.
The function has not been touched in 2,167 days. There is no recent institutional knowledge here. My recommendation: before any future work in this file, extract the ignore-pattern bailout, the relative/absolute path normalization, and the package-name lookup into named helper functions. That alone would reduce both the cyclomatic complexity and the fan-out of resolveToUnqualified to something more tractable, and it would give each branch a name that documents its intent.
applyNodeExtensionResolution — generate-pnp-map-api.tpl.js
At cyclomatic complexity 26 — the highest of any function in the top 5 — applyNodeExtensionResolution implements Node’s file-extension probe logic inside the PnP runtime. The source excerpt shows an infinite while loop (the comment is the author’s own) that restarts whenever it encounters a package folder, calling statSync on each candidate path, reading package.json for a main field, and then probing each supported extension in sequence. It even recurses into itself when a package.json main field redirects to a new path.
The CC of 26 means there are 26 independent paths through this function. Pair that with the complex_branching and exit_heavy patterns and you have a function that is structurally hard to reason about and expensive to cover with tests. The nesting depth of 4 is at the threshold where most engineers need to actively track context to understand which branch they are in. The recursion adds another dimension: a path through the function can re-enter it, multiplying the effective state space.
Fan-out is 14, touching filesystem primitives (statSync, lstatSync, readlinkSync, existsSync, readFileSync) and path utilities, which means any change to how the function probes the filesystem has to be coordinated across all those call sites.
This function has been dormant for 2,167 days. My recommendation: separate the symlink-resolution logic, the package.json main-field lookup, and the extension-probe loop into three distinct functions. The while(true) restart pattern suggests the loop invariant is not obvious — naming the termination condition explicitly would be a good first step toward reducing the CC.
resolveRequest — generate-pnp-map-api.tpl.js
resolveRequest is the public-facing entry point that sequences the full resolution pipeline: it calls resolveToUnqualified first, then resolveUnqualified, and handles the error cases that arise when either step fails. The source excerpt shows a layered try/catch structure where a BUILTIN_NODE_RESOLUTION_FAIL error triggers a retry with a realpathSync-resolved issuer path, and a second try/catch on the unqualified resolution step augments errors with request/issuer context before rethrowing.
A cyclomatic complexity of 20 against a fan-out of only 7 tells me this function’s complexity is almost entirely in its error-handling and control-flow logic rather than in the number of collaborators it calls. The nested try/catch blocks, the conditional retry path, and the multiple rethrow points are what push CC to 20. The exit_heavy and stale_complex patterns reinforce this: there are many ways to exit, and the function has been sitting at this complexity level, untouched, for six years.
Because resolveRequest is the function external callers invoke, any misunderstanding of its error-handling contract ripples directly into user-facing behavior. My recommendation: extract the symlink-retry logic (the BUILTIN_NODE_RESOLUTION_FAIL branch) into a named helper. That one extraction would drop the CC of resolveRequest noticeably and make the retry intent legible without reading through the nested catch body.
setup — generate-pnp-map-api.tpl.js
setup is the function that hooks PnP into Node’s module loading system by monkey-patching Module._load and Module._resolveFilename. Its cyclomatic complexity of 7 is the lowest among the four critical functions — the branching here is modest. What pushes its risk score to 11.44 is the fan-out of 20: the function reaches out to 20 distinct callees to wire up the hook, manage the module cache, handle the pnpapi special case, disable native hooks around builtins, apply module patches via patchedModules, and set the main module on process.
The god_function and long_function patterns reflect what the source confirms: setup is orchestrating a broad set of behaviors in one place. In JavaScript, patching Module._load is particularly high-stakes — it intercepts every require() call in the process. A fan-out of 20 at this intercept point means a wide surface of collaborators all need to behave correctly for every module load to succeed. Dynamic dispatch through Module._load and Module._resolveFilename also means the static fan-out likely understates the true coupling at runtime.
With 2,167 days since last change and no recent authorship context, there is no memory of why each of the 20 callees is invoked in the order it is. My recommendation: document the sequencing contract explicitly — why builtins are handled before pnpapi, why the cache check precedes module construction, why patchedModules runs last. That documentation would reduce the cognitive load for the next engineer substantially, even before any structural refactoring.
getCallee — scripts/eslint-rules/warn-language.js
getCallee lives in a custom ESLint rule that enforces message formatting conventions on reporter log calls and MessageError throws. The function itself walks a CallExpression AST node to find the callee, unwinding nested MemberExpression chains with a while loop before checking that the root object is a reporter identifier.
At CC 11 with nesting depth of 1 and fan-out of 0, this function’s complexity is entirely self-contained — no external dependencies, just conditional branching over AST node types. The zero fan-out is actually a positive signal here: getCallee is a pure predicate over its input with no side effects or collaborators to reason about. The CC of 11 is above the moderate threshold of 10, but the function is short and the branching is a direct reflection of the AST shape it needs to handle.
This function has not been touched in 2,167 days and carries no external defect signals. It is the furthest from a refactoring priority among the five. The main concern I would flag is that the while loop unwinding MemberExpression chains has 11 paths through it — if the ESLint rule is ever extended to cover additional callee shapes, that CC will climb. My recommendation: add inline comments naming each guard condition (e.g. why callee.computed is an early exit) so future contributors understand the bounds before extending the logic.
Patterns Found
Antipatterns detected across the top functions in this snapshot:
| Pattern | Occurrences |
|---|---|
exit_heavy | 4 |
complex_branching | 3 |
god_function | 3 |
long_function | 3 |
stale_complex | 3 |
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/yarnpkg/yarn
cd yarn
git checkout c2dda503f3759b5be5f0e24ecd9cf5c97a540147
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 →