Harness's pull request UI carries the highest activity risk — 5 functions to fix

Five critical-band functions in harness/harness's pull request and pipeline UI are both structurally complex and actively changing, with cyclomatic complexity reaching 90 and nesting depth hitting 15 in a single React component.

Stephen Collins ·
Generated by hotspots · free & open source
pip
$ pip install hotspots-cli
Activity Risk19.74Low
Hottest FunctionusePullReqComments

Antipatterns Detected

complex_branching5deeply_nested5god_function5long_function5exit_heavy4middle_man1

Run this on your own codebase

See if your own repo has a usePullReqComments-style hotspot — run this in any local git repo:

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

Key Points

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

A god function is one that has accumulated responsibility for so many distinct concerns that it becomes the integration point for an entire feature — characterized by high fan-out (the count of distinct functions it directly calls) and high cyclomatic complexity relative to its single unit of functionality. In harness, all five top hotspots carry this pattern: `usePullReqComments` calls 111 distinct functions, `AddUpdatePipeline` calls 57, and `MarkdownViewer` calls 53. The practical problem is blast radius: because these functions couple to so many callees, a change to any one of those dependencies — a renamed prop, a modified hook signature, a new API response shape — requires reasoning about the entire god function to assess impact. They are also the hardest functions to test in isolation, because setting up all 111 (or 57, or 53) dependencies is prohibitively expensive, so tests tend to cover happy-path surface only.

How do I reduce cyclomatic complexity and fan-out in a React TypeScript codebase?

The most effective first step is the extract-method refactoring applied at hook and utility boundaries: pull self-contained logic out of a large component or hook into a named function or custom hook that can be tested independently. A cyclomatic complexity above 15 warrants splitting; above 30 it warrants immediate attention — `ChangesSection` at 90 and `usePullReqComments` at 81 are both well past that threshold. For `usePullReqComments` specifically, extracting the DOM-mutation logic (`updateDataSelectedCount`) and the comment API wiring into separate hooks would cut both the cyclomatic complexity and the fan-out at the top-level function boundary in a single refactoring pass. Fan-out reduction follows naturally from complexity reduction: when a function delegates to smaller, focused collaborators rather than calling everything directly, the number of distinct callees at the top level drops, and each extracted piece becomes independently mockable in tests.

Is harness actively maintained?

Yes — the quadrant data is unambiguous on this point. All 4,365 high-complexity functions in the repository are in the 'fire' quadrant, meaning every one of them has recent commit activity; there are zero functions in the 'debt' quadrant. All five top hotspots were touched within the last 30 days, each recording one commit in that window, with the most recent change 27 days ago. Active development and high structural complexity are not mutually exclusive — harness is clearly a product under continuous development, and the structural debt visible in these metrics is the accumulated cost of building quickly in a feature-rich domain, not a sign of neglect.

How do I reproduce this analysis?

The Hotspots CLI is available at github.com/hotspots-dev/hotspots. To reproduce this exact analysis, check out the repository at commit `157c934` with `git checkout 157c934`, then run `hotspots analyze . --mode snapshot --explain-patterns --force` from the repository root. The same command runs on any local git repository without additional configuration and will produce function-level risk scores comparable to what is shown here.

What does activity-weighted risk mean?

Activity-weighted risk combines a function's structural complexity — derived from cyclomatic complexity, nesting depth, and fan-out — with how frequently that function has been changed in recent commits. A function with cyclomatic complexity 81 that has not been touched in two years scores lower than one with cyclomatic complexity 21 that is touched every week, because the complex-but-dormant function presents a lower near-term probability of introducing a regression. The score prioritizes functions where complexity and change frequency overlap, because that combination is where bugs are most likely to be introduced by the next commit. In harness at this snapshot, the top score of 19.74 for `usePullReqComments` reflects both its extreme structural complexity and its presence in an actively-developed part of the codebase — making it a live regression risk rather than a cleanup item to schedule for later.

At commit 157c934, harness/harness has 13,307 analyzed functions, 1,002 of which score in the critical band — and every function in the top five is a ‘fire’-quadrant case, structurally complex and touched within the last 30 days. I would start with usePullReqComments in web/src/components/DiffViewer/usePullReqComments.tsx, which carries an activity-weighted risk score of 19.74 — the highest in the repository — driven by a cyclomatic complexity of 81 and a fan-out of 111 distinct function calls. That combination means any engineer shipping a change to the diff viewer this week is working inside a function that has 81 independent execution paths and couples to 111 other callees. Harness is a developer platform covering code review, CI/CD pipelines, and artifact management; the fact that its most urgent structural risk is concentrated in the pull request UI, not in its Go services, is the first thing worth understanding.

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
usePullReqCommentsweb/src/components/DiffViewer/usePullReqComments.tsx19.7817111
ChangesSectionweb/src/pages/PullRequest/Conversation/PullRequestOverviewPanel/sections/ChangesSection.tsx19.7901525
AddUpdatePipelineweb/src/pages/AddUpdatePipeline/AddUpdatePipeline.tsx17.135557
MarkdownViewerweb/src/components/MarkdownViewer/MarkdownViewer.tsx16.950553
usePRChecksDecisionweb/src/hooks/usePRChecksDecision2.tsx16.521815

Large Repo Analysis

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

Repository overview

Triage Band Distribution
Fire4365Watch8942

13,307 functions analyzed

The quadrant distribution here is striking: every analyzed function is either ‘fire’ or ‘watch’ — there are zero ‘debt’ and zero ‘ok’ functions. That means the repository has no dormant structural complexity sitting undetected; all the high-complexity code is also the actively-changing code. For engineers triaging where to spend refactoring time, this makes the priority list unusually clean: the critical band is also the live-regression band.

Detected Antipatterns
Complex Branching×5Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.
Deeply Nested×5Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.
God Function×5God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.
Long Function×5Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.
Exit Heavy×4Exit Heavy
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.
Middle Man×1Middle Man
Mostly delegates to one other function without adding meaningful logic — a refactoring candidate for removal or consolidation.

All five top hotspots share every major antipattern: complex branching, deep nesting, god-function coupling, excessive length, and — in four of the five — multiple exit paths that expand test-coverage surface. These are not isolated quirks; they form a consistent structural signature across the pull request and pipeline UI.


usePullReqComments — usePullReqComments.tsx

usePullReqComments
web/src/components/DiffViewer/usePullReqComments.tsx
19.74
fire
CC 81
ND 7
FO 111
touches/30d 1

This React hook orchestrates comment creation, update, deletion, link-copying, and DOM mutation for the diff viewer — all inside a single function body. The source excerpt confirms it: it wires together useCommentAPI, usePullReqActivities, useConfirmAct, useToaster, useStrings, useAppContext, and route helpers before it even begins the main comment-management logic. That accounts for a fan-out of 111, the highest in the top five and a strong god-function signal — a change to any one of those 111 callees can surface as unexpected behavior here.

The cyclomatic complexity of 81 means there are 81 independent execution paths through this hook. In a React context, many of those paths live inside useCallback and useMemo closures, which makes them harder to reach in isolation with unit tests. The nesting depth of 7 is visible in the excerpt’s updateDataSelectedCount callback, which branches on DOM presence, then on selection state, then on count thresholds — nested conditionals that each add to the path count.

The exit-heavy pattern compounds this: multiple early returns scattered across closures mean coverage tools will report line coverage without actually exercising all decision points. Combined with only a single author having touched this file in the last 90 days, the institutional knowledge required to safely modify this hook is concentrated in one person.

Recommendation: Extract the DOM-mutation logic (updateDataSelectedCount and any similar row-state helpers) into a standalone utility, and pull the comment API wiring into a dedicated useCommentOperations hook. That alone would reduce the fan-out visible at the top level and bring the cyclomatic complexity below 50 — halving the test-path burden without changing any behavior.


ChangesSection — ChangesSection.tsx

ChangesSection
web/src/pages/PullRequest/Conversation/PullRequestOverviewPanel/sections/ChangesSection.tsx
19.68
fire
CC 90
ND 15
FO 25
touches/30d 1

With a cyclomatic complexity of 90 and a max nesting depth of 15, ChangesSection is the structurally densest function in the repository’s top five. It renders the approval-and-review-decision panel on the pull request overview page, and the source excerpt reveals why the numbers are this high: it simultaneously resolves code-owner approval state, reviewer approval state, default-reviewer policy state, outdated-SHA detection, and change-request status — all in a single component body, before a single pixel is rendered.

A nesting depth of 15 is a strong refactoring signal by any standard. Visually, that means conditional blocks nested inside filter callbacks nested inside reduce operations nested inside conditional renders — chains of logic where the outer context is easy to lose track of by the time you reach the innermost branch. The extractInfoForCodeOwnerContent inner function visible in the excerpt is itself doing multi-level branching, which contributes directly to that depth count.

The exit-heavy pattern here — four of the five top functions carry it, and this one is no exception — means there are multiple return paths that short-circuit the render cycle under different approval conditions. Each one is a required test case that is rarely written in practice for components this large.

Max Nesting Depth 15
threshold: 4

Recommendation: Decompose the approval-state derivation into separate selector functions — one for code-owner state, one for reviewer state, one for default-reviewer policy — and call them from the component rather than computing inline. The component itself should only be responsible for choosing which UI state to render, not for deriving all the inputs to that decision. This would reduce nesting depth to well below 8 and make each approval-logic path independently testable.


AddUpdatePipeline — AddUpdatePipeline.tsx

AddUpdatePipeline
web/src/pages/AddUpdatePipeline/AddUpdatePipeline.tsx
17.14
fire
CC 35
ND 5
FO 57
touches/30d 1

This component manages the full lifecycle of creating or editing a CI pipeline: fetching pipeline metadata, loading YAML file content from the repository, determining whether the pipeline already exists, detecting YAML version (v0 Drone config vs. v1), tracking editor dirty state, and wiring up save/run/save-and-run action options. The source excerpt confirms all of this happens in a single component body, with multiple useEffect hooks managing side effects sequentially.

A fan-out of 57 is the god-function signature in a different form from usePullReqComments — instead of 111 callees, here we have 57, but the blast-radius principle is the same: the component is coupled to the routing layer, the repository metadata layer, the pipeline API, the YAML content API, the editor reference, the toaster, and the modal system simultaneously. A cyclomatic complexity of 35 is high (the threshold for immediate attention), but it is lower than the two PR-review functions above because the branching is spread across useEffect callbacks rather than concentrated in a single control-flow block.

The long_function and exit_heavy patterns here suggest the component body is doing work that belongs in custom hooks. The useEffect chain for YAML version detection, existence checking, and initial content loading are each independently testable pieces of logic that currently share the component’s closure scope.

Recommendation: Extract the pipeline-load and YAML-resolution logic into a usePipelineEditor hook. This would reduce the component’s fan-out to the hook boundary, lower the cyclomatic complexity of the component itself to near single digits, and give the load/resolve logic its own test surface without React rendering overhead.


MarkdownViewer — MarkdownViewer.tsx

MarkdownViewer
web/src/components/MarkdownViewer/MarkdownViewer.tsx
16.88
fire
CC 50
ND 5
FO 53
touches/30d 1

MarkdownViewer renders repository markdown with support for image zoom, mention substitution, suggestion blocks, and routing-aware link rewriting. The source excerpt shows the mention-substitution logic in detail: a custom rehype plugin (rehypeReplaceMentions) that walks the AST of every rendered paragraph, pattern-matching against a three-node sequence of text, anchor, and text children to find @[email] mention syntax and replace it with display names — including XSS sanitization of the display name before injection.

A cyclomatic complexity of 50 here reflects how many distinct content scenarios the component must handle: standard markdown, images (with zoom state), mentions (with email-to-name resolution), suggestion blocks with checksums, dark mode, max-height constraints, and routing context for relative links. Each of these adds branching, and the AST-walk loop adds more — the for loop over paragraph children with a three-way type-check guard contributes multiple paths per paragraph node.

The fan-out of 53 indicates the component reaches broadly into the application’s utilities and plugin ecosystem. A change to the mention resolution logic, the routing helpers, or the suggestion-block rendering can each introduce regressions that only manifest in specific markdown content combinations — exactly the kind of bug that is hard to catch without comprehensive content-fixture tests.

Recommendation: Pull rehypeReplaceMentions out of the component body into a standalone, exportable rehype plugin file. It has no dependency on React state and can be unit-tested against raw AST fixtures without mounting a component. That extraction alone reduces the component’s cyclomatic complexity by removing the entire AST-traversal branch tree, and makes the XSS-sanitization logic independently auditable.


usePRChecksDecision — usePRChecksDecision2.tsx

usePRChecksDecision
web/src/hooks/usePRChecksDecision2.tsx
16.48
fire
CC 21
ND 8
FO 15
touches/30d 1

The filename usePRChecksDecision2.tsx is worth pausing on — the 2 suffix suggests this is a second iteration of an earlier hook, implying the team has already been through one version. That naming pattern often signals an API that is still settling, and a cyclomatic complexity of 21 in a hook that is actively being iterated on is a live regression risk: the next change to CI check-status handling lands in code that already has 21 independent execution paths.

The source excerpt shows the structural driver: a switch statement over ExecutionState values inside a for loop over check results, followed by a cascading if/else if chain that maps count combinations to color, background, and message state. A nesting depth of 8 comes from that combination — the switch inside the for inside the useMemo callback, with the if/else if chain at the same depth. The TODO comment in the excerpt (// TODO: This needs to be revised, as green is shown initially) confirms the maintainer is already aware of a correctness concern in the state initialization, which makes the complexity here a live issue rather than abstract debt.

Cyclomatic Complexity 21
threshold: 10

Recommendation: Replace the if/else if cascade over count combinations with a priority-ordered lookup table — an array of [condition, result] pairs evaluated in order. This converts O(N) branching into a data-driven structure that is easier to read, extend, and test, and it directly reduces the cyclomatic complexity. Address the TODO around initial state before the next change lands; the interaction between initial complete: true state and the async data fetch is a latent correctness risk in any execution path where data arrives slowly.


What the pattern distribution tells me

Every function in the top five carries the god-function pattern — not because any individual one does something unusual, but because the pull request and pipeline UI in harness aggregates a large number of concerns at component and hook boundaries rather than delegating them. Fan-out values of 111, 57, and 53 are not outliers; they reflect a consistent architectural pattern where the top-level component or hook is the integration point for everything its feature needs.

That is a rational short-term approach to building a complex product quickly. The cost shows up in these metrics: when the integration point is also the place where business logic, DOM manipulation, API calls, and rendering all live together, every change to any one of those concerns touches a function that is already hard to reason about in its entirety. The four exit-heavy functions in this list mean that test suites covering these components line-by-line are still missing decision-point combinations that can only be reached by specific sequences of user interaction or data state.

The context-only functions — getString, useQueryParams, onSuccess, useRoutes, clear — are all ‘watch’-quadrant: active but structurally simple. They bear monitoring but are not refactoring priorities at their current complexity levels.

Patterns Found

Antipatterns detected across the top functions in this snapshot:

PatternOccurrences
complex_branching5
deeply_nested5
god_function5
long_function5
exit_heavy4
middle_man1

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/harness/harness
cd harness
git checkout 157c934a790d264b254f64e1762df85bac045d25
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 →

Was this useful? Let me know →

Related Analyses