At commit 3513044, fyne-io/fyne has 4838 analyzed functions, 175 of which sit in the critical risk band. Every single function in the quadrant breakdown falls into either fire or watch — zero functions are in debt or ok quadrants — which means the structural risk in this codebase is concentrated in code that is actively moving. I would start with processMouseClicked in internal/driver/glfw/window.go, which earned 4 commits in the last 30 days against a cyclomatic complexity of 18 and a fan-out of 20, making it a live regression risk rather than a cleanup item. The broader picture across the top five is that 821 fire-quadrant functions are in play simultaneously, spanning the GLFW input driver, the software painter, the markdown widget, and the packaging toolchain.
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 |
|---|---|---|---|---|---|
makeLostContextSimulatingCanvas | cmd/fyne/internal/templates/data/webgl-debug.js | 18.0 | 14 | 13 | 39 |
renderNode | widget/markdown.go | 15.7 | 22 | 3 | 18 |
validateAppID | cmd/fyne/internal/commands/package.go | 15.7 | 7 | 8 | 6 |
drawShadow | internal/painter/software/draw.go | 15.3 | 10 | 5 | 29 |
processMouseClicked | internal/driver/glfw/window.go | 15.0 | 18 | 5 | 20 |
Large Repo Analysis
fyne 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 function makeLostContextSimulatingCanvas lives in cmd/fyne/internal/templates/data/webgl-debug.js, which is a bundled copy of the Khronos WebGL debug utility included as template data for fyne’s web export toolchain. Its nesting depth of 13 and fan-out of 39 reflect the JavaScript closure patterns used in that utility, not application logic written by the fyne maintainers. To exclude it from future Hotspots runs, add the following to your .hotspotsrc.json: { "exclude": ["cmd/fyne/internal/templates/data/"] }. This will suppress the entire template asset directory without affecting analysis of the Go codebase.
Quadrant and Pattern Overview
4,838 functions analyzed
The absence of any debt or ok quadrant functions is the first thing I notice. Every complex function in fyne is being touched. That shifts the framing from “where is the hidden debt?” to “which active surfaces are most likely to regress under the current development pace?”
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.Deeply Nested×4Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.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.Long Function×4Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.Complex Branching×3Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.
Across the five highest-scoring functions, exit-heavy and deeply-nested are the dominant structural antipatterns. Exit-heavy functions — those with many distinct return paths — impose a multiplicative test-coverage burden: every path is a branch the test suite must reach independently. Deeply-nested control structures make it hard to reason about invariants at any given point in the call stack. Four of the five functions also qualify as god functions or long functions, meaning the blast radius of a single change is unusually wide.
makeLostContextSimulatingCanvas — cmd/fyne/internal/templates/data/webgl-debug.js
This is the highest activity-risk score in the analysis at 18.02, driven almost entirely by a nesting depth of 13 and a fan-out of 39. It is also a JavaScript function — see the vendor note below for the appropriate context on this file and how to exclude it if it is cluttering your triage queue.
From the source excerpt, the function is a WebGL debug utility that wraps a canvas element to simulate context-loss events. It builds up a wrappedContext_ object, intercepts canvas.getContext via closure, hooks addEventListener for the webglcontextlost and webglcontextrestored event types, and exposes methods like loseContext. The nesting depth of 13 comes from the combination of immediately-invoked function expressions, nested closure assignments, and event dispatch logic — patterns common in pre-module JavaScript tooling. The fan-out of 39 reflects how many WebGL API methods the wrapper must proxy.
The file has 1 total commit, 1 author in the last 90 days, and no bug-linked history. This is bundled tooling data, not application logic. I am noting it here only because it topped the raw scoring table — after exclusion, it drops off the list entirely.
renderNode — widget/markdown.go
With a cyclomatic complexity of 22, renderNode has the highest CC of any pure Go function in the top five. It received 1 commit in the last 30 days and was last modified 25 days ago — still fire quadrant, actively changing, but toward the quieter end of the active range.
The source excerpt makes the complexity immediately legible: renderNode is structured as a large type-switch over AST node types from the goldmark Markdown parser. Each case arm handles a different node kind — Document, Paragraph, List, ListItem, TextBlock, Heading, ThematicBreak, Link, AutoLink, CodeSpan, CodeBlock, FencedCodeBlock, Emphasis, Strikethrough, Text, Blockquote — and each arm has its own error handling, segment construction, and recursive calls to renderChildren or sibling render functions. The ast.Text case alone branches on empty text, soft line breaks, and quoting depth, contributing multiple paths to the CC count.
Fan-out of 18 reflects the breadth of the RichTextSegment subtypes being constructed: TextSegment, ListSegment, ParagraphSegment, HyperlinkSegment, CheckBoxSegment, SeparatorSegment, and others all get instantiated here. A change to any of those types’ constructors or fields has a plausible path back through this function.
The file history is clean — 3 total commits, no bug-linked commits, no reverts — so there is no historical defect signal pushing this up. The risk is purely structural and forward-looking: as the Markdown widget gains new node types (task lists, footnotes, tables), they will be added to this switch, and the CC will climb further.
My recommendation is to apply an extract-method pass, moving each case arm into its own renderParagraph, renderList, renderText, etc. function. The switch itself becomes a dispatcher with near-zero logic per arm, and each render sub-function can be tested in isolation.
validateAppID — cmd/fyne/internal/commands/package.go
This is the structural outlier in the top five. Its cyclomatic complexity of 7 is the lowest of any hotspot here — moderate by most standards — but its max nesting depth of 8 is what drives the score. A nesting depth of 8 is a strong refactoring signal: at that depth, a reader must track 8 levels of conditional context simultaneously to understand what any single line means.
The source excerpt confirms this directly. The function validates app bundle IDs across platforms — darwin, iOS, Android, Windows-for-release — using a cascade of if os == ... / else if branches. The Android path is where the nesting peaks: it first checks for an empty appID, then for the presence of a dot, then for a hyphen, then splits on dots and iterates over package name segments, checking each segment’s first byte against '_' and '0'–'9' ranges. That inner loop is sitting 7–8 levels deep in nested conditionals by the time it runs.
Fan-out of 6 is low, so coupling is not the concern here. The function was touched once in the last 30 days and last modified 25 days ago, putting it in the fire quadrant — live regression risk if packaging feature work continues at this pace.
The fix I would reach for first is an early-return / guard-clause rewrite. Rather than nesting inside if os == "android", validate each constraint at the top of its own block and return immediately on failure. That flattens the structure from ND 8 to roughly ND 3–4 without changing any behavior. A platform-dispatch table keyed on OS string, with each platform’s validator as a small function, would go further.
drawShadow — internal/painter/software/draw.go
The headline metric for drawShadow is its fan-out of 29 — the highest of any Go function in the top five. Twenty-nine distinct callees means that a change to drawShadow’s behavior, or a change to any one of those 29 functions, has a plausible ripple path through this function. It was touched once in the last 30 days and last modified 22 days ago, placing it in the fire quadrant as an actively changing function.
From the source, the fan-out is structurally motivated: the function dispatches on the concrete type of the canvas object (Rectangle, Circle, Ellipse) via a type-switch, and for each shape it constructs both a shadow image and a mask image by calling into dedicated painter functions (painter.DrawRectangle, painter.DrawCircle, painter.DrawEllipse), applies a Gaussian blur, computes screen coordinates via scale helpers, intersects with the clip rectangle, and then conditionally subtracts the object shape from the shadow for the DropShadow variant. Each of those steps pulls in a different callsite.
The CC of 10 and ND of 5 are moderate on their own — the type-switch and the padding arithmetic branching account for most of it — but combined with 29 fan-out and the god_function and long_function tags, this is a function that is doing per-shape construction, coordinate math, compositing, and shadow-variant logic all in one body.
No bug-linked commits or reverts appear in the file history, so I would frame this as a maintainability concern rather than a defect risk. The concrete recommendation is to extract the per-shape shadow-and-mask construction into separate functions (buildRectangleShadow, buildCircleShadow, buildEllipseShadow), leaving drawShadow as an orchestrator that handles coordinate math and compositing. That would reduce both fan-out and length considerably.
processMouseClicked — internal/driver/glfw/window.go
This function is the most actively changing of the five Go functions. Four commits touched it in the last 30 days, and it was last modified 16 days ago. That alone would warrant attention, but the structural picture makes it more pressing: a cyclomatic complexity of 18 means 18 independent execution paths, a nesting depth of 5 puts it above the level where control flow becomes hard to reason about locally, and a fan-out of 20 means it directly calls 20 distinct functions — a broad coupling surface across the GLFW driver.
From the source excerpt, I can see why all three metrics land where they do. The function coordinates a large number of concerns in a single body: it resolves cursor position when the window lacks focus, dispatches hit-testing to find the object under the mouse, handles Mouseable, Focusable, Tappable, SecondaryTappable, DoubleTappable, and Draggable interfaces via a series of type assertions and conditional branches, manages drag start and drag end lifecycle, and decides whether to unfocus the currently focused widget — all before the excerpt ends. The type-switch and nested conditionals around focus management are where the nesting depth peaks.
The file-level history adds context without being alarming: 14 total commits on window.go, a bug-fix fraction of 14.3%, and 4 distinct authors in the last 90 days. That author spread combined with the current commit rate means multiple engineers are reasoning about this function concurrently — a coordination risk on top of the structural one.
The concrete action I would take first is to extract the focus-management decision (the block that checks w.canvas.Focused(), runs a second hit-test, and conditionally calls w.canvas.Unfocus()) into its own named function. That alone would reduce the cyclomatic complexity by several paths and make the drag-vs-tap dispatch logic easier to follow in isolation.
Patterns Found
Antipatterns detected across the top functions in this snapshot:
| Pattern | Occurrences |
|---|---|
exit_heavy | 5 |
deeply_nested | 4 |
god_function | 4 |
long_function | 4 |
complex_branching | 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.
See more analyses with these patterns: complex_branching, deeply_nested, exit_heavy, god_function, long_function.
Reproduce This Analysis
git clone https://github.com/fyne-io/fyne
cd fyne
git checkout 3513044e39e9cbf7e455fc13d06e71210db778e2
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 →