validator.js spans 204 analyzed functions, 30 of them landing in the critical band, and the top risk isn’t a function under active churn — it’s isURL, which hasn’t been modified in 264 days but still carries a cyclomatic complexity of 58, a fan-out of 27, and an activity-weighted risk of 16.97. All five functions in my top-5 table fall into the ‘debt’ quadrant: zero commits in the last 30 days, but structural complexity high enough that the next person who changes one of them takes on real blast radius with no recent context to lean on. I’d start with isURL, not because it’s on fire, but because it’s the largest, most tangled piece of debt sitting closest to the top of the pile.
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 |
|---|---|---|---|---|---|
isURL | src/lib/isURL.js | 17.0 | 58 | 5 | 27 |
isEmail | src/lib/isEmail.js | 15.0 | 37 | 3 | 29 |
normalizeEmail | src/lib/normalizeEmail.js | 14.9 | 29 | 6 | 14 |
isMobilePhone | src/lib/isMobilePhone.js | 14.6 | 13 | 6 | 7 |
isDate | src/lib/isDate.js | 14.5 | 20 | 3 | 15 |
The shape of the risk
204 functions analyzed
Out of 204 functions, 68 fall into the debt quadrant — structurally complex, but not currently being touched. Only 4 sit in the fire quadrant (actively changing and complex), and those 4 live entirely in isTaxID.js, which I cover briefly below. The dominant story in this repo is dormant complexity, not live churn.
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.Complex Branching×5Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.God Function×4God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.Stale Complex×4Stale Complex
High structural complexity but untouched for a long time — structural debt that will bite whoever opens it next.Deeply Nested×3Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.Long Function×2Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.Hub Function×1Hub Function
Many other functions call this one — a change here ripples widely through callers.
Exit-heavy is the most common antipattern across the top hotspots (6 occurrences), followed by complex branching (5) and god function (4). That combination — many return points, many execution paths, wide coupling — is what turns a quiet file into a maintenance trap the day someone needs to add a new option flag.
isURL — src/lib/isURL.js
isURL is the top-ranked hotspot in the repo, and it earns that spot on structure alone: cyclomatic complexity of 58 against a fan-out of 27, tagged god_function, long_function, deeply_nested, and exit_heavy. The source shows why — a chain of early-return guards (mailto detection, fragment/query stripping, length checks) followed by an inline protocol-parsing routine that has to disambiguate protocol: from user:password@host auth strings, including a check for URL-encoded content. That’s a lot of decision logic living in one function, and it hasn’t been touched in 264 days, with no distinct authors committing to it in the last 90 days either. The file’s own commit history shows five of its eight total commits were bug fixes — a high historical bug-fix rate. None of that proves today’s code is broken, but it does mean this function has been fragile before. My recommendation: pull the protocol-vs-auth disambiguation logic (the cleanUpProtocol closure and the @// position checks) into a named, independently testable helper before the next feature request forces someone to read all 58 paths at once.
isEmail — src/lib/isEmail.js
isEmail carries the highest fan-out of the five, at 29 distinct function calls, against a cyclomatic complexity of 37. Nesting depth is comparatively shallow at 3, which tells me the complexity here is driven by breadth — display-name parsing, host allow/deny lists, Gmail-specific normalization, byte-length checks — rather than deep conditional stacking. It’s tagged exit_heavy, long_function, and god_function, and it hasn’t changed in 632 days. Two of its three recorded commits were bug fixes. The Gmail-specific branch inside this function — dot-stripping, sub-address removal, username-part validation via gmailUserPart — is already a self-contained unit; extracting it into its own named function would cut both the complexity count and the fan-out attributed to isEmail itself.
normalizeEmail — src/lib/normalizeEmail.js
normalizeEmail has the deepest nesting of any function in the top five — a max nesting depth of 6 — which lines up with the source: a long if/else-if chain branching on domain (gmail, icloud, outlook.com, yahoo, yandex), each branch with its own nested option checks for subaddress removal and lowercasing. It’s tagged complex_branching and deeply_nested together, the strongest signal in this dataset that the control flow itself is the problem, not just the line count. It’s been dormant for 720 days, but all three of its recorded commits were bug fixes. That’s a small sample, but it’s a 100% hit rate on a function that only gets touched to fix something. The cleanest fix here is decompose-conditional: turn the five domain-specific branches into a lookup table of normalizer functions (normalizeGmail, normalizeIcloud, etc.) keyed by domain, so adding or fixing a provider’s rules doesn’t require re-reading the whole chain.
isMobilePhone — src/lib/isMobilePhone.js
isMobilePhone is the outlier in this group: a cyclomatic complexity of 13 is modest next to the others, but its nesting depth of 6 matches normalizeEmail’s, meaning the risk here comes from how deep the control flow goes, not how many branches exist at the top level. The source shows an array-locale path with a nested .some() callback wrapping a property lookup and a regex test — three levels of indirection to answer one boolean question. It’s been untouched for 784 days, the longest dormancy of the five, and its past pull requests drew noticeably more review comments per change than the other functions here, even though only 12 total commits have touched the file. That combination — long dormancy plus a history of dense review conversation — suggests this function is worth a second look even without recent bug-fix commits (about a third of its commits were fixes, the lowest rate of the group). I’d flatten the nested locale-lookup logic into a single loop with an early return instead of the current mixed .some()/for...in structure.
isDate — src/lib/isDate.js
isDate carries the hub_function tag — the only one in this dataset — alongside god_function, exit_heavy, and stale_complex. Fan-out is 15, and the source shows why: format-string parsing, delimiter matching, per-field length validation, two-digit-year expansion logic, and a fallback path for non-string Date objects all live in one function. A hub function means a lot of other code paths likely route through this one, so a change to the year/month/day normalization logic carries more ripple potential than the complexity number alone suggests. It’s been dormant for 657 days, with two of its three recorded commits fixing bugs. My first move would be to extract the two-digit-year expansion (fullYear handling) and the delimiter-matching logic into separate, named functions — both are self-contained blocks that don’t need to see the rest of isDate’s state.
What’s actually active right now
The fire quadrant in this repo is small — 4 functions — and all four sit outside the top-5 table: dkDkCheck, lvLvCheck, and plPlCheck in src/lib/isTaxID.js, plus utf8ByteLength in isByteLength.js. Each was committed to once in the last 30 days and last changed 15 to 21 days ago, meaning tax-ID validation logic is where real development is happening right now. None cracked the top 5 because their structural complexity (cyclomatic complexity 11–23, fan-out 1–7) is lower than the five debt-quadrant functions above — but they’re worth watching precisely because they’re moving targets. If any of them climbs in complexity while still under active edit, that’s the profile that becomes a fire-quadrant top-5 entry next time this analysis runs.
Patterns Found
Antipatterns detected across the top functions in this snapshot:
| Pattern | Occurrences |
|---|---|
exit_heavy | 6 |
complex_branching | 5 |
god_function | 4 |
stale_complex | 4 |
deeply_nested | 3 |
long_function | 2 |
hub_function | 1 |
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: complex_branching, deeply_nested, exit_heavy, god_function, stale_complex.
Reproduce This Analysis
git clone https://github.com/validatorjs/validator.js
cd validator.js
git checkout a79ff980ab14257e795332989e497bdff3218e87
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 →