dayjs's core formatter carries the highest structural debt — 5 functions to address first

In iamkun/dayjs, the format function in src/index.js leads all 524 analyzed functions with a CC of 38 and fan-out of 13, flagged as stale, complex structural debt with high blast radius on next change.

Stephen Collins ·
oss javascript refactoring code-health

Antipatterns Detected

exit_heavy5god_function3long_function2stale_complex2

Key Points

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

A god function is one that has accumulated too many responsibilities — it calls many other functions, handles multiple concerns, and becomes the load-bearing pillar that everything else leans on. In dayjs, both the core format function and the objectSupport plugin wrapper are flagged as god functions: format reaches into 13 other functions while the plugin wrapper reaches into 21. When a god function needs to change, the blast radius is wide — you risk breaking callers or callees you didn't intend to touch, and the cognitive load of understanding all its responsibilities before editing safely is high.

How do I reduce cyclomatic complexity in JavaScript?

The most direct technique is extract-method refactoring: identify clusters of branches that share a single decision concern and pull them into a named function with a clear contract, then replace the original branching block with a call to that function. For lookup-heavy cases like locale translation (CC 51 in src/locale/sl.js), replacing chains of conditionals with a data-driven lookup table can collapse dozens of branches into a single indexed access.

Is dayjs actively maintained?

The structural complexity in dayjs is real — 34 critical-band functions out of 524 — but all five top hotspots fall in the 'debt' quadrant, meaning none of them are in active churn right now. The risk is accumulated structural complexity waiting for the next development push, not a sign of ongoing instability.

How do I reproduce this analysis?

Run the hotspots CLI against the iamkun/dayjs repository at commit ea2929d to reproduce the exact scores shown here.

What does activity-weighted risk mean?

Complexity × recent commit frequency — functions that are hard to understand AND actively changing are the highest priority for refactoring.

Of 524 functions analyzed in iamkun/dayjs at commit ea2929d, 34 land in the critical band — and the highest-ranked hotspot, format in src/index.js, hasn’t been actively changed recently but carries a cyclomatic complexity of 38 and fans out to 13 callees, meaning the blast radius when it is next touched is substantial. Every top-5 function carries high structural complexity but low recent activity — the risk is accumulated debt, not current churn. With 34 critical and 26 high-band functions across a 524-function codebase, the concentrated risk in src/index.js and the plugin layer deserves attention before the next development push.

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
formatsrc/index.js12.738113
<anonymous>src/plugin/objectSupport/index.js12.515221
translatesrc/locale/sl.js12.55122
<anonymous>src/index.js12.45216
<anonymous>src/plugin/relativeTime/index.js12.39314

Hotspot Analysis

format — src/index.js

The format function in src/index.js is almost certainly the core date-formatting engine — the central path any caller takes to render a dayjs object as a string. A cyclomatic complexity of 38 means 38 independent execution paths, each a required test case and a potential bug surface; fan-out of 13 means it reaches into 13 other functions, giving it broad coupling across the codebase. It has not been actively touched recently, but the stale_complex and god_function patterns flag it as accumulated structural debt with a high blast radius the moment formatting behavior needs to change. The exit_heavy pattern adds a test-coverage burden — multiple return paths mean any incomplete test suite leaves dark corners.

Recommendation: Add a comprehensive characterization test suite covering all 38 execution paths before any modification, then use extract-method refactoring to break format into smaller, single-responsibility helpers (e.g. one per token type), reducing both CC and fan-out in the core function.

<anonymous> — src/plugin/objectSupport/index.js

The anonymous top-level function in src/plugin/objectSupport/index.js is most likely the plugin registration wrapper that extends dayjs with object-argument support — a common pattern in the dayjs plugin architecture. Despite a moderate cyclomatic complexity of 15, its fan-out of 21 is the highest in the top five, meaning this single function reaches into 21 distinct callees; changes here can produce unexpected ripple effects across a wide surface. Like format, it has not been actively modified recently — but the god_function and exit_heavy patterns indicate it has accumulated responsibility and exit paths that will be costly to reason through when the plugin next needs updating.

Recommendation: Map the 21 fan-out targets to understand which are truly owned by this function versus merely called through it, then extract discrete delegation helpers to reduce coupling and make the plugin’s extension points individually testable.

translate — src/locale/sl.js

The translate function in src/locale/sl.js is the Slovenian locale’s pluralization and string-selection logic — a function that must handle the grammatical complexity of Slovenian noun cases, which is one of the more demanding in any i18n system. A cyclomatic complexity of 51 is high: it places this function among the most branchy in the entire codebase, with 51 independent paths driven almost entirely by grammatical rules rather than fan-out (fan-out is just 2). The exit_heavy pattern confirms that many of those paths terminate in early returns, making full test coverage genuinely difficult. This is pure structural debt — it hasn’t been recently touched, but its complexity means any future locale correction or pluralization fix carries significant regression risk.

Recommendation: Replace the conditional chains with a data-driven lookup table keyed by grammatical category and word form. This collapses most of the 51 branches into a single indexed access, making the pluralization rules easier to audit and extend. Scope any future locale correction narrowly — 51 paths cannot be meaningfully reviewed in a standard PR, so regression tests covering Slovenian noun-case edge cases are essential before any change lands.

<anonymous> — src/index.js

The anonymous function in src/index.js ranked fourth has a cyclomatic complexity of 52 — the highest CC in the top five — with a nesting depth of only 1 and fan-out of 6. That combination (very high CC, low ND, low FO) means the complexity is wide and shallow: many parallel cases handled inline rather than nested or delegated. This is likely the dayjs constructor or parser — the function that initializes a dayjs instance from an input argument and must handle numbers, strings, arrays, objects, and Date instances in separate branches. The stale_complex and long_function patterns confirm it has grown by accretion and hasn’t been recently modified. With 52 independent paths, any incomplete test suite leaves parsing edge cases unverified.

Recommendation: Adopt a dispatch-table approach: consolidate branch clusters into a map from input type to a single-responsibility parsing handler (one per input type: number, string, array, object, Date). This reduces CC by replacing the flat conditional with a lookup and makes each parsing path independently testable.

<anonymous> — src/plugin/relativeTime/index.js

The anonymous function in src/plugin/relativeTime/index.js has a modest CC of 9 but the deepest nesting in the top five (ND 3) and the second-highest fan-out after objectSupport (FO 14). This is most likely the core of the relativeTime plugin — translating a numeric time difference into a locale-aware string like “a few minutes ago.” Its complexity is coordination complexity: it orchestrates 14 callees across threshold logic and locale formatting, with nested conditionals managing the edge cases between thresholds. The god_function pattern flags this as a function that owns too many concerns for its size.

Recommendation: Break the function into two phases: (1) a pure numeric threshold calculator that returns a keyed token (e.g. 'minutes', 'hours'), and (2) a formatting layer that delegates to the active locale. This separation makes the threshold logic independently testable, reduces the fan-out of the core function, and mirrors how well-structured i18n libraries handle relative time.

Patterns Found

Antipatterns detected across the top functions in this snapshot:

PatternOccurrences
exit_heavy5
god_function3
long_function2
stale_complex2

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.

Key Takeaways

  • The format function in src/index.js has CC 38 and fan-out 13 — write characterization tests covering all execution paths before any future formatting change to avoid silent regressions.
  • The anonymous function in src/plugin/objectSupport/index.js has a fan-out of 21, the highest in the top five; audit its 21 callees to identify which responsibilities can be extracted into focused helpers before the plugin is next modified.
  • The translate function in src/locale/sl.js has CC 51 driven by grammatical branching, not coupling — scope any locale fix narrowly and ensure pluralization edge cases are regression-tested, since 51 paths cannot be meaningfully reviewed in a standard PR review.
  • The anonymous constructor/parser in src/index.js (CC 52) handles every input type in a single flat function — a dispatch table keyed by input type would reduce CC and make each parsing path independently testable.
  • The relativeTime plugin’s core function has fan-out 14 and nesting depth 3 — separating threshold calculation from locale formatting would reduce coordination complexity and make the threshold logic testable in isolation.

Reproduce This Analysis

git clone https://github.com/iamkun/dayjs
cd dayjs
git checkout ea2929d2c9aaba4a6766d8954e0dc37c24f8e5a2
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.

Hotspots highlights structural and activity risk — not “bad code.” Findings are a prioritization aid, not a bug predictor. Editorial policy →

Run this on your own codebase

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

macOS
$ brew install Stephen-Collins-tech/tap/hotspots
Linux / cargo
$ cargo install hotspots-cli
Run in any repo
$ hotspots analyze .
★ Star on GitHub

Related Analyses