Of the 6,506 functions Hotspots analysed in opentofu/opentofu at commit 8b22295, 831 land in the critical band — and every single top-five function sits in the fire quadrant, meaning structural complexity and recent commit activity overlap right now. The headline function, coerceValue in internal/configs/configschema/coerce_value.go, carries an activity-weighted risk score of 17.92, with a cyclomatic complexity of 32 and calls out to 51 distinct functions — a combination that makes any change to it a live regression risk this week, not a theoretical one. I would start there, then work through the two near-identical waitForRun implementations and MarshalResourceChanges before tackling the StateMvCommand.Run god-function.
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 |
|---|---|---|---|---|---|
coerceValue | internal/configs/configschema/coerce_value.go | 17.9 | 32 | 6 | 51 |
waitForRun | internal/backend/remote/backend_common.go | 17.1 | 17 | 7 | 22 |
waitForRun | internal/cloud/backend_common.go | 17.1 | 17 | 7 | 22 |
MarshalResourceChanges | internal/command/jsonplan/plan.go | 17.0 | 29 | 5 | 44 |
Run | internal/command/state_mv.go | 16.6 | 11 | 6 | 75 |
Large Repo Analysis
opentofu 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.
Quadrant and pattern overview
6,506 functions analyzed
Every function in this codebase falls into either fire or watch — there is no dormant structural debt sitting untouched and no truly quiet corners. 2,333 functions are fire-quadrant: high complexity and recent activity. That is not a sign of a poorly maintained project; it reflects a codebase that is large, actively developed, and carrying real structural weight in its core subsystems.
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.Exit Heavy×5Exit Heavy
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.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.
Every one of the five top hotspots shares the same five antipatterns simultaneously: complex branching, deep nesting, many exit paths, god-function coupling, and excessive length. That unanimity is the signal worth pausing on — it means the risk is not scattered but concentrated in a specific architectural style that recurs across schema handling, remote backend coordination, plan serialisation, and state management.
coerceValue — coerce_value.go
This function is responsible for taking a raw cty.Value and coercing it to conform to a block’s declared schema — handling nulls, unknowns, type mismatches, required vs optional vs computed attributes, and recursive block nesting, all in one body. From the source excerpt I can see it opens with a type-switch on null and unknown sentinels, then validates that every incoming attribute is declared, sorts attribute and block-type keys for deterministic error ordering, and recurses into nested block types via a second switch on nesting mode (NestingSingle, NestingGroup, and by extension the modes not shown). Every branch that rejects input returns a cty.UnknownVal plus a path-annotated error — the excerpt alone shows at least seven distinct early-return paths.
With a cyclomatic complexity of 32, there are at minimum 32 independent execution paths through this function. Each one is a required test case; each one is a place where a future schema change can introduce a silent regression. The max nesting depth of 6 means a reviewer has to hold six levels of conditional context simultaneously to reason about the innermost branches. The fan-out of 51 is the most striking number: this function reaches into more than fifty distinct callees, from cty type introspection to convert.Convert to recursive calls back into itself. That breadth means a change to any one of those callees — a new cty value kind, a change to convert semantics — can alter coerceValue’s behaviour without touching this file at all.
It was touched 1 time in the last 30 days and last changed 20 days ago, making it a live surface. The external signals show no bug-linked commits and no reverts on this file, so there is no historical defect record to amplify the concern — but the structural profile alone justifies priority attention.
Recommendation: Split coerceValue by responsibility phase. The attribute coercion loop and the block-type coercion loop are already structurally distinct — each can become its own function (coerceAttributes and coerceBlockTypes). The null/unknown sentinel checks at the top are a natural third extraction. Targeting those three extractions would roughly halve the CC and cut the per-function fan-out, making each piece independently testable against the specific schema combinations it handles.
waitForRun — backend_common.go (remote) and backend_common.go (cloud)
These two functions are structurally identical — same activity-weighted risk score of 17.14, same CC of 17, same max nesting depth of 7, same fan-out of 22 — and the source excerpts confirm they differ only in the receiver type (*Remote vs *Cloud) and minor view-call details. Both poll a remote run to completion using a backoff loop, managing two cancellation contexts (stopCtx and cancelCtx), workspace lock state, queue position calculation across paginated run lists, and elapsed-time display logic, all interleaved inside a single infinite loop.
The Go concurrency angle matters here. The outer select on stopCtx.Done(), cancelCtx.Done(), and time.After(...) is the correct pattern for cancellable polling — but with a nesting depth of 7, the lock-detection branch (w.Locked && w.CurrentRun != nil) and the paginated run-list scan (runlist: labelled for-loop) are buried several levels deep inside timing and status checks. In Go, deeply nested control flow around channel selects and error returns is a known readability hazard: a reviewer has to mentally unwind seven levels to confirm that every error path correctly returns the right run reference and error value, and that context cancellation cannot be silently swallowed by an inner branch.
The near-perfect duplication across two packages is itself a maintenance risk: any fix applied to one implementation must be manually mirrored to the other. Both were touched once in the last 30 days and last changed 20 days ago.
Recommendation: Extract the queue-position calculation into a shared helper — that is the most complex inner branch and it is identical across both files. Then consider whether the remote and cloud backends can share a single waitForRun implementation through a narrow interface, eliminating the duplication entirely. Even without full consolidation, pulling the paginated run-list scan into its own function would reduce the nesting depth from 7 to roughly 4 in both copies and make the cancellation logic easier to audit.
MarshalResourceChanges — plan.go
This function converts a slice of plans.ResourceInstanceChangeSrc values into the JSON plan representation consumed by external tooling — the structured output that powers tofu show -json, automation pipelines, and third-party integrations. From the source excerpt I can see it handles sorting for deterministic output, special-cases ephemeral resources by nullifying their before/after values, skips delete actions on data sources, resolves provider schemas, decodes change sources, strips marks for intermediate processing, then re-encodes before/after values as JSON with sensitivity metadata attached. Each of those responsibilities introduces its own branching: the excerpt alone shows at minimum five distinct early-return error paths and multiple conditional blocks around cty.NilVal, IsWhollyKnown, and mark stripping.
A cyclomatic complexity of 29 in a serialisation function is a test-coverage problem as much as a readability problem. JSON plan output is a stability contract for every tool that consumes tofu show -json; a regression in any of the 29 paths — say, incorrect sensitivity metadata for ephemeral resources — can silently corrupt downstream pipeline data without an obvious error. Fan-out of 44 means this function is coupled to schema resolution, cty JSON encoding, sensitivity helpers, mark utilities, and address formatting simultaneously. The external signals are clean — no bug-linked commits, no reverts — but this is the kind of function where a bug would be subtle and user-visible.
Recommendation: The per-resource processing block inside the main loop is long enough to warrant extraction into a marshalSingleResourceChange helper. That alone would drop the top-level CC significantly and make the sort-and-iterate skeleton readable in isolation. The ephemeral-resource nullification and the sensitivity-metadata assembly are each further candidates for named helpers, which would also make the serialisation contract easier to unit-test against specific resource change types.
Run — state_mv.go
StateMvCommand.Run is the entry point for tofu state mv — argument parsing, view setup, version checking, backend detection, encryption configuration, state loading, address parsing, state mutation, and output rendering are all orchestrated here. Its cyclomatic complexity of 11 is the lowest in the top five, but its fan-out of 75 is the highest in the entire list by a wide margin. That combination — moderate branching, extreme coupling — is the signature of a command dispatcher that has absorbed responsibility over time rather than delegating it.
The source excerpt shows this directly: flag parsing, diagnostic rendering with new-line configuration, legacy-backend detection for -backup and -backup-out flags, encryption initialisation, and backend loading all happen before the actual state move logic begins. The comment in the excerpt — // TODO meta-refactor: when we move the backend logic to its own component… — is an in-code acknowledgment of the debt. With 75 distinct callees and a nesting depth of 6, this function has a blast radius that spans argument parsing, backend infrastructure, encryption, and state management simultaneously. Any change to how backends are resolved, how encryption is initialised, or how diagnostics are reported touches this function.
Recommendation: The TODO comment points at the right long-term fix. In the near term, extracting the legacy-backend detection logic (the -backup/-backup-out flag handling and isLocalBackend check) into a named helper would remove several branches and reduce fan-out without requiring the larger meta-refactor. The encryption initialisation block is similarly self-contained and could be extracted with a clear name and return signature, making the top-level flow read as a sequence of named phases rather than inline imperative code.
Supporting context
Five functions from context_only round out the picture without reaching the structural severity of the top five. ProposedNew and proposedNewObjectAttributes in internal/plans/objchange/objchange.go are watch-quadrant — low structural complexity but touched in the last 30 days, worth keeping an eye on as the objchange layer evolves. checkForSensitiveType and checkForSensitiveNestedAttribute in internal/command/jsonformat/differ/sensitive.go follow the same pattern: recently active, low CC, but sitting adjacent to the sensitive-data handling path that MarshalResourceChanges also touches. delete in internal/configs/configschema/decoder_spec.go shares a file neighbourhood with coerceValue and is worth monitoring if work continues in the configschema package.
Codebase Risk Distribution
All five top hotspots share the same structural patterns (complex_branching, deeply_nested, exit_heavy, god_function, long_function), which is typical of the highest-risk functions in any large codebase — they accumulate every structural signal on the way to the top. More useful context is how the risk is distributed across all 6,506 analyzed functions:
| Band | Functions |
|---|---|
| Critical | 831 |
| High | 1,502 |
| Moderate | 3,006 |
| Low | 1,167 |
Hotspot patterns 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/opentofu/opentofu
cd opentofu
git checkout 8b222957589638103c72155cfb67add244b88546
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 →