Leaflet's GeoJSON and map core carry the highest activity risk — 5 to address first

Analysis of Leaflet/Leaflet at ff38fe5 finds geometryToLayer and _findEventTargets in active 'fire' quadrant alongside a vendored highlight.pack.js inflating the critical count — here is what actually needs attention.

Stephen Collins ·
Generated by hotspots · free & open source
Activity Risk14.79Low
Hottest Functionf

Antipatterns Detected

exit_heavy4complex_branching2god_function2deeply_nested1

Run this on your own codebase

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

pip
$ pip install hotspots-cli
npm
$ npm install -g @stephencollinstech/hotspots
Run in any repo
$ hotspots analyze .
★ Star on GitHub

Key Points

What is an exit-heavy function and why does it matter in Leaflet?

An exit-heavy function has multiple distinct return or throw statements scattered through its body rather than a single exit point — each one represents an independent execution path that must be reached and verified by a test. In Leaflet, four of the top hotspots carry this pattern, including `geometryToLayer` with 27 independent paths and `_update` with 21. The practical problem is test-coverage cost: exercising every path through `geometryToLayer` requires constructing test inputs for Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon, GeometryCollection, FeatureCollection, missing geometry, and the error default — and the recursive cases multiply that. Any path left uncovered is a potential regression site when the function is next changed.

How do I reduce cyclomatic complexity in JavaScript?

The most effective first step is the extract-method refactoring: identify one coherent sub-decision inside the function and move it into a named private function, which removes its branches from the parent's path count entirely. For `geometryToLayer` (CC 27), extracting the collection-iteration logic for `GeometryCollection` and `FeatureCollection` into a `_layersFromCollection` helper would remove at least four paths from the switch. A CC above 15 warrants splitting; above 25, as with `geometryToLayer`, it warrants immediate attention. The decompose-conditional technique — replacing inline `if/else-if` chains with named predicate functions — is also effective for `_prepareOpen` (CC 13, ND 4), where the latlng-resolution chain can become a single named call.

Is Leaflet actively maintained?

The commit data at this snapshot shows clear active development: `_findEventTargets` in `src/map/Map.js` received 4 commits in the last 30 days and was last changed 2 days ago, and `flyTo` in the same file also shows 4 touches in 30 days. At the same time, three of the top real-code hotspots — `_update` in GridLayer.js, and the debt-quadrant functions `_onDown` and `_off` — have not been touched in 47 days, pointing to structural debt that is accumulating without active maintenance pressure. Active development and structural debt are not mutually exclusive; the picture here is a codebase under genuine ongoing work with a few complex subsystems that have gone quiet.

How do I reproduce this analysis?

The Hotspots CLI is available at github.com/hotspots-dev/hotspots. Check out Leaflet at the analyzed commit with `git checkout ff38fe5`, then run `hotspots analyze . --mode snapshot --explain-patterns --force` from the repository root. The same command works on any local git repository without additional configuration — no `.hotspotsrc.json` is required to get started, though adding one with an exclude pattern for `docs/docs/highlight/` will remove the vendored highlight.js from the results.

What does activity-weighted risk mean?

Activity-weighted risk combines a structural complexity score — derived from cyclomatic complexity, nesting depth, and fan-out — with recent commit frequency, so that functions which are both hard to understand and actively changing score the highest. A function with cyclomatic complexity 80 that has not been touched in two years scores lower than one with CC 20 touched every week, because the dormant function poses lower near-term regression risk even though it looks more complex in isolation. The practical value of this prioritization is that it surfaces where bugs are most likely to be introduced right now, not just where the code is hardest to read — `_findEventTargets` with 4 commits in 30 days and a CC of 20 is a better use of review time than an equally complex function that no one is changing.

Across 956 functions analyzed at commit ff38fe5, Leaflet carries 20 critical-band functions — and the two that concern me most are geometryToLayer in src/layer/GeoJSON.js and _findEventTargets in src/map/Map.js, both in the ‘fire’ quadrant: structurally complex and actively changing today. _findEventTargets has been touched 4 times in the last 30 days against a cyclomatic complexity of 20, and src/map/Map.js has accumulated 15 bug-linked commits historically with a bug-fix fraction of 50% — that combination argues for treating it as a live regression surface, not a cleanup item for the next sprint. The headline activity-weighted risk score of 13.53 for geometryToLayer (touched once in 30 days, CC 27) makes it the top real-code priority once the vendored documentation asset is set aside.

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
fdocs/docs/highlight/highlight.pack.js14.826323
geometryToLayersrc/layer/GeoJSON.js13.52735
_updatesrc/layer/tile/GridLayer.js12.821316
_findEventTargetssrc/map/Map.js12.02034
_prepareOpensrc/layer/DivOverlay.js11.91345

Large Repo Analysis

Leaflet 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.

Codemod / Tooling Files in Results

The top-scoring function by raw activity_risk is f inside docs/docs/highlight/highlight.pack.js, and a second critical-band function s appears in the same file. Both are part of the bundled highlight.js syntax highlighter shipped with Leaflet’s documentation — not application code. Their structural complexity scores reflect the minified, multi-language parser packed into that file, and their single commit in history reflects a one-time vendor update. To remove them from future analyses, add the following to .hotspotsrc.json: { "exclude": ["docs/docs/highlight/"] }. This will suppress all functions in that directory without affecting any Leaflet source analysis.

Quadrant distribution

Triage Band Distribution
Fire44Debt45Watch341OK526

956 functions analyzed

Detected Antipatterns
Exit Heavy×4Exit Heavy
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.
Complex Branching×2Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.
God Function×2God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.
Deeply Nested×1Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.

Of the 956 functions Hotspots measured, 44 land in the ‘fire’ quadrant — high complexity and actively changing — while 45 sit in ‘debt’: structurally risky but untouched long enough that the immediate regression risk is lower. The 526 ‘ok’ functions can be ignored for now. The action is concentrated in a surprisingly small slice of the codebase.

Before getting into the real hotspots, one entry needs to be flagged and excluded.


f — highlight.pack.js (vendored — see vendor note)

The highest raw activity-weighted risk score of 14.79 belongs to a function inside docs/docs/highlight/highlight.pack.js. This is the bundled highlight.js library shipped with Leaflet’s documentation site. It has not been touched in the last 30 days and its single commit history reflects a one-time vendor update, not ongoing development. I would exclude this file before acting on any score it produces. See the vendor note at the bottom for the exact ignore pattern.


geometryToLayer — GeoJSON.js

geometryToLayer
src/layer/GeoJSON.js
13.53
critical
CC 27
ND 3
FO 5
touches/30d 1

This static method is the primary entry point for converting a GeoJSON object — Feature, GeometryCollection, FeatureCollection, Point, LineString, Polygon, and their Multi variants — into Leaflet layer objects. The source excerpt confirms it does this through a large switch on geometry.type, with recursive self-calls for GeometryCollection and FeatureCollection, early-exit guard clauses at the top, and seven distinct return paths. That is precisely what produces a cyclomatic complexity of 27: every case branch, every conditional guard, and each recursive path is an independent execution path that needs its own test.

Cyclomatic Complexity 27
threshold: 10

The exit_heavy pattern is visible directly in the code — there is a return null for missing geometry, a return for each geometry type in the switch, and a throw for the default case. That makes complete branch coverage genuinely expensive: 27 independent paths means 27 minimum test cases to exercise every one of them, and the recursive branches for nested geometry collections multiply that surface further.

The external signals add context: both commits to this file have been associated with a bug fix, meaning every change to geometryToLayer has historically been a correction rather than a feature. That does not prove the function is broken, but it does suggest the branching logic has historically been a source of confusion. The function was last touched 16 days ago and has 1 commit in the last 30 days — it is in the fire quadrant and actively being changed.

Recommendation: The recursive handling of GeometryCollection and FeatureCollection could be extracted into a dedicated private method, separating the “dispatch by geometry type” concern from the “collect layers from a collection” concern. That split alone would cut the path count roughly in half and make the recursive case independently testable.


_update — GridLayer.js

_update
src/layer/tile/GridLayer.js
12.81
critical
CC 21
ND 3
FO 16
touches/30d 0

This is a debt-quadrant function: it has not been touched in 47 days and has zero commits in the last 30. The structural numbers are what demand attention here. A fan-out of 16 is the highest among the real-code hotspots — this function directly calls into bounds calculation, tile coordinate conversion, validity checking, DOM fragment construction, tile addition, event firing, and view resetting.

Fan-out (distinct callees) 16
threshold: 8

The source excerpt shows why: _update does everything from clamping the zoom level and computing pixel bounds, to iterating the existing tile cache to mark stale tiles, to building a priority-sorted queue of tiles to load, to appending DOM fragments. The god_function and exit_heavy patterns both apply — there are multiple early returns for guard conditions, and the function orchestrates the entire tile refresh lifecycle in one place.

Historically, src/layer/tile/GridLayer.js carries 9 bug-linked commits. Again, that is a file-level signal, not proof of a defect in _update specifically — but it does mean that when someone next changes this function, they will be working in a file with a non-trivial bug history.

Because it is dormant right now, this is not a fire-drill. But its blast radius when next touched is significant: 16 callees means a change here can ripple into tile coordinate math, DOM structure, event dispatch, and zoom-level logic simultaneously.

Recommendation: Before the next development push on GridLayer, extract the tile-queue population loop (the nested for j / for i block that validates and enqueues tile coordinates) into a private _buildTileQueue method. That reduces the CC of _update materially and gives the queue-building logic a seam for independent testing.


_findEventTargets — Map.js

_findEventTargets
src/map/Map.js
12.05
critical
CC 20
ND 3
FO 4
touches/30d 4

This is the function I would treat as the most urgent real-code priority in the entire repository. It has been touched 4 times in the last 30 days — more recent commit activity than any other hotspot — and was last changed just 2 days ago. With a cyclomatic complexity of 20, every iteration of that while loop over the DOM parent chain carries multiple branching conditions: hover vs. non-hover event types, dragging state, listener presence, and container boundary checks all interact.

Cyclomatic Complexity 20
threshold: 10
Commits in last 30 days 4
threshold: 2

The source excerpt shows the structure: a while (src) loop that walks up the DOM, checking _targets, drag state, listener registration, and hover semantics at each step, with several break conditions and a fallback that assigns the map itself as the sole target. In JavaScript, this kind of prototype-based dispatch combined with event type string comparisons is exactly where subtle ordering bugs can appear — and the file-level signals confirm this is a known pressure point: 15 bug-linked commits, a bug-fix fraction of 0.50 (half of all commits to this file tagged as bug fixes), and a PR review comment density of 2.6, which is the highest in this dataset. Four active contributors in the last 90 days means ownership is spread, increasing the chance that two people reason differently about the interaction between drag suppression and hover semantics.

Recommendation: Extract the hover-event short-circuit logic and the drag-suppression check into named predicate functions so the while-loop body reads as a sequence of named decisions rather than inline conditionals. This directly reduces the CC and makes each decision independently testable — which matters here because the bug-fix history suggests the existing test coverage has missed edge cases before.


_prepareOpen — DivOverlay.js

_prepareOpen
src/layer/DivOverlay.js
11.93
critical
CC 13
ND 4
FO 5
touches/30d 1

This fire-quadrant function handles the setup logic before an overlay (popup or tooltip) is displayed: it resolves the source layer from a potential FeatureGroup, derives an anchor latlng by probing the source for getCenter, getLatLng, or getBounds, and triggers a content update if the overlay is already on the map. The complex_branching pattern is visible in the source — after the FeatureGroup resolution block, there is a four-way if/else-if chain probing different layer capabilities to find a usable position, terminating in a throw if none matches.

Max Nesting Depth 4
threshold: 4

The nesting depth of 4 is the most notable structural signal here. The FeatureGroup iteration loop nests inside an instanceof check, and the getCenter/getLatLng/getBounds chain nests inside the !latlng guard — four levels deep in a function whose job is coordinate resolution. At ND 4, reasoning about which path the code takes for a given combination of source type and latlng argument requires tracking the full nesting context.

There are no bug-linked commits on this file and no reverts, so this is more a maintainability concern than a historical defect signal. With 1 touch in 30 days, it is live but not churning.

Recommendation: Extract the latlng-resolution block — the probe for getCenter, getLatLng, and getBounds — into a private _resolveLatLng(source) helper. This flattens the nesting, gives the resolution logic a name that documents its intent, and makes it straightforward to add a new layer type without touching the open-preparation flow.


What else is worth watching

Without writing full sections for them, two context_only functions are worth a brief mention. addOne in src/dom/DomEvent.js — touched once in the last 30 days with a nesting depth of 5 and the deeply_nested pattern — sits just outside the top five at an activity_risk of 11.84, and is part of the same event-handling subsystem as _findEventTargets. If work on Map.js event dispatch is planned, addOne is a natural companion review target. flyTo in src/map/Map.js, with 4 touches in the last 30 days and a fan-out of 18 (the god_function pattern), is the other active pressure point in Map.js and would benefit from the same refactoring attention as _findEventTargets given they live in the same file and share the same commit churn.

Patterns Found

Antipatterns detected across the top functions in this snapshot:

PatternOccurrences
exit_heavy4
complex_branching2
god_function2
deeply_nested1

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/Leaflet/Leaflet
cd Leaflet
git checkout ff38fe55d31c05aabb6a2bd3cb948748d4e4b793
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 →

Related Analyses