yazi: actor, shim, and CLI layers carry the highest risk — 5 functions to fix first

Four of yazi's five highest-risk functions are in the 'fire' quadrant — structurally complex and actively changing — spanning the actor, terminal shim, and CLI layers of the Rust codebase.

Stephen Collins ·
Generated by hotspots · free & open source
pip
$ pip install hotspots-cli
Activity Risk14.61Low
Hottest Functionr#do

Antipatterns Detected

exit_heavy8complex_branching4deeply_nested1long_function1hub_function1

Run this on your own codebase

See if your own repo has a r#do-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 an exit-heavy function and why does it matter in yazi?

An exit-heavy function is one with a large number of distinct return points — early returns, error bail-outs, and conditional exits scattered throughout the body rather than converging to a single exit path. The concrete problem is test coverage: each return path is an independent execution scenario that requires its own test case to verify correct behavior. In yazi's top hotspots, eight of the analyzed functions carry this pattern, including `r#do` in `bulk_rename.rs` and `parse_osc5522` in `osc.rs`. When exit-heavy functions are also actively changing — as both of those are, with `r#do` touched 3 days ago and `parse_osc5522` touched 9 days ago — each new commit risks missing a return path that existing tests don't exercise, which is exactly when regressions slip through.

How do I reduce cyclomatic complexity in Rust?

The most effective technique in Rust is extract-method refactoring: identify cohesive groups of branches — a nested match arm, a loop body, an error-handling block — and move them into named private functions. A cyclomatic complexity above 15 is a reasonable threshold to start extracting; above 30, splitting is overdue. For `value_to_data` in `yazi-shared/src/data/sendable.rs`, which has a CC of 55, a concrete first step is extracting the `Table` match arm into a `table_value_to_data` function — it already calls `value_to_key` recursively and has a clear input/output contract that makes it independently testable. That one extraction alone would reduce the CC of `value_to_data` by roughly 15 paths. For the `run` function in `yazi-cli/src/main.rs` (CC 39), moving each command arm to its own `handle_*` async function is the equivalent move.

Is yazi actively maintained?

Yes — the fire-quadrant data makes that clear. Of the five highest-risk functions, four are in the fire quadrant, meaning they are both structurally complex and receiving recent commits. `r#do` in `bulk_rename.rs` was touched twice in the last 30 days and modified just 3 days before this analysis. `run` in `yazi-cli/src/main.rs` was touched 13 days ago. Active development and structural debt are not mutually exclusive: the 194 debt-quadrant functions — including `value_to_data`, untouched for 43 days, and `valid_wtf8`, untouched for 109 days — represent accumulated complexity that hasn't been prioritized for refactoring. Neither is receiving active commits; they are structural debt waiting on the next engineer who needs to extend them.

How do I reproduce this analysis?

The Hotspots CLI is available at github.com/hotspots-dev/hotspots. This analysis was run against sxyazi/yazi at commit `caa7797`. To reproduce it, check out that commit with `git checkout caa7797` and 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.

What does activity-weighted risk mean?

Activity-weighted risk combines a function's structural complexity — derived from its cyclomatic complexity, nesting depth, and fan-out — with how frequently it has been changed by recent commits. A function with a cyclomatic complexity of 55 that hasn't been touched in 43 days scores lower than a function with a cyclomatic complexity of 16 that was committed to twice in the last month, because the dormant function poses less near-term regression risk even though it's harder to understand in the abstract. This prioritization is designed to focus attention where bugs are most likely to be introduced right now — at the intersection of structural complexity and active change — rather than simply surfacing the most complicated code in the repository.

At commit caa7797, yazi has 4,710 analyzed functions, 82 of which score in the critical band. Four of the five highest-risk hotspots sit in the fire quadrant — meaning they are both structurally complex and actively changing right now, not candidates for a future sprint. The top-ranked function, r#do in yazi-actor/src/mgr/bulk_rename.rs, carries an activity-weighted risk score of 14.61, has a cyclomatic complexity of 16 with 11 distinct callees, and was touched twice in the last 30 days. The one outlier in the top five is value_to_data in yazi-shared, which sits in the debt quadrant: untouched for 43 days, 0 commits in the last 30, but carrying a CC of 55 — structural debt with a high blast radius when it eventually gets changed. I’d treat the fire-quadrant functions as this week’s review priority and schedule value_to_data for decomposition before the next feature push touches the Lua data layer.

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
r#doyazi-actor/src/mgr/bulk_rename.rs14.616411
nextyazi-shim/src/ratatui/span.rs14.32651
parse_osc5522yazi-term/src/parser/osc.rs14.022310
value_to_datayazi-shared/src/data/sendable.rs13.65533
runyazi-cli/src/main.rs13.43925

Risk distribution

Triage Band Distribution
Fire217Debt194Watch1051OK3248

4,710 functions analyzed

Detected Antipatterns
Exit Heavy×8Exit Heavy
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.
Complex Branching×4Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.
Deeply Nested×1Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.
Long Function×1Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.
Hub Function×1Hub Function
Many other functions call this one — a change here ripples widely through callers.

The distribution tells an interesting story: 217 functions in the fire quadrant means active commits are landing in structurally complex code across the repo right now. The 194 debt-quadrant functions are a parallel concern — untouched but intricate, waiting to bite the next engineer who needs to change them. The dominant antipattern across the top hotspots is exit-heavy code: eight of the top functions have multiple early-return paths, each of which is a test case the test suite may not cover.


r#do — bulk_rename.rs

r#do
yazi-actor/src/mgr/bulk_rename.rs
14.61
fire
CC 16
ND 4
FO 11
touches/30d 2

The reserved-keyword name r#do is itself a signal: this is the async entry point that drives the bulk-rename operation — comparing old and new filename lists, acquiring a filesystem watcher permit, iterating rename candidates, and dispatching success or failure paths. The source excerpt confirms a function that does a lot: it validates list lengths, filters unchanged pairs, calls prioritized_paths, acquires an async semaphore, loops over rename candidates with multi-branch error handling per iteration, publishes post-rename events via Pubsub, and conditionally surfaces failure output.

With a cyclomatic complexity of 16 and 11 distinct callees (fan-out of 11), this function touches a broad surface: watcher acquisition, URL manipulation, filesystem engine calls, event publishing, and TTY output. The nesting depth of 4, combined with complex_branching and exit_heavy patterns, means the iteration body alone contains at least four distinct outcome paths per rename candidate — and the exit_heavy classification indicates multiple early returns at the function’s outer level too. That combination makes exhaustive unit testing difficult without mocking most of the callee surface.

What sharpens the urgency is the activity signal: 2 commits in the last 30 days, with the most recent just 3 days ago. This is live code under active modification. The file’s history shows bug fixes in roughly 1 of every 10 commits, which is not alarming on its own, but any regression introduced into the rename loop — particularly in the maybe_exists / must_identical / rename branch logic — affects user data directly.

Recommendation: Extract the per-candidate rename logic (the inner loop body) into a dedicated rename_one function. That single step reduces the CC of r#do by roughly a third and makes the three outcome paths (skip existing, engine error, success) independently testable without standing up a full watcher or TTY context.


next — span.rs

next
yazi-shim/src/ratatui/span.rs
14.3
fire
CC 26
ND 5
FO 1
touches/30d 1

This is an Iterator::next implementation — Rust’s standard iterator protocol — living in yazi’s ratatui shim layer. The excerpt shows it dispatching across at least three enum variants (Span, Line, and Wrapped), with the Span and Line arms each containing their own loop, tab-expansion state management (pending_tabs), control-character filtering, and style-patching logic. The Line arm is the more complex of the two: it manages a current span cursor, drains graphemes from it, handles tab expansion with pending-tab bookkeeping, and advances to the next span from the outer iterator when the current one is exhausted.

Cyclomatic Complexity 26
threshold: 10

A CC of 26 with a nesting depth of 5 in an iterator implementation is a strong refactoring signal. The deeply_nested and complex_branching patterns both fire here. The fan-out of 1 tells you this function doesn’t call much externally — the complexity is entirely internal branching, which means the 26 independent execution paths are all baked into the control flow of a single function body. Each path is a required test case for correct grapheme rendering, and with 1 commit touching this in the last 30 days (10 days ago), it’s actively changing.

Because this is in yazi-shim/src/ratatui/, it sits at the boundary between yazi’s rendering logic and the ratatui terminal library — a seam where correctness matters for every character drawn to the terminal. Any regression in tab handling or control-character filtering is immediately user-visible.

Recommendation: Split the Line arm logic into a dedicated private method — something like next_from_line — that handles span-cursor advancement and tab expansion independently. The Span arm’s tab logic is simpler and could share a small helper. That extraction would bring the CC of next below 15 and make the tab-expansion behavior testable in isolation.


parse_osc5522 — osc.rs

parse_osc5522
yazi-term/src/parser/osc.rs
13.97
fire
CC 22
ND 3
FO 10
touches/30d 1

The function name encodes the protocol: OSC 5522 is a custom terminal escape sequence, and this is the parser for it. From the source excerpt, it strips the OSC terminator, splits the sequence into a metadata segment and a payload, then iterates over colon-separated key-value pairs in the metadata — dispatching on keys like type, loc, mime, pw, and status. Base64 decoding (via STANDARD_PAD_INDIFFERENT) appears for both MIME type and password fields. After parsing metadata, it enforces size limits (16 MiB total payload, 1024 MIME entries) and conditionally decodes and accumulates the binary payload.

Cyclomatic Complexity 22
threshold: 10

The CC of 22 comes from the branching within the metadata key dispatch, the size-limit guards, and the payload accumulation logic — all in one function. The fan-out of 10 means it touches a broad set of helpers: UTF-8 conversion, base64 decode, state mutation, error construction. The exit_heavy pattern is consistent with what the excerpt shows: multiple early error returns scattered through the metadata loop and the payload section. Two authors have touched this file in the last 90 days — when more than one engineer is navigating a parser with 22 paths, a shared mental model of the state machine matters.

The size-limit checks (16 << 20 for payload, 1024 for MIME count) are security-adjacent: they guard against maliciously crafted or malformed sequences blowing up memory. That logic deserves explicit test coverage for the boundary conditions, which the current structure makes awkward to achieve without running the full parser.

Recommendation: Extract the metadata key dispatch into a separate apply_osc5522_meta method and the payload accumulation into accumulate_osc5522_payload. That decomposition isolates the security-relevant size checks into a function that can be unit tested with synthetic state, without needing a full parser context.


value_to_data — sendable.rs

value_to_data
yazi-shared/src/data/sendable.rs
13.61
debt
CC 55
ND 3
FO 3
touches/30d 0

This is the only debt-quadrant function in the top five, and its CC of 55 makes it the most structurally complex entry in the entire list.

Cyclomatic Complexity 55
threshold: 10

value_to_data converts a Lua Value into yazi’s internal Data type — a translation layer between mlua’s dynamic value system and the typed Rust domain. The source excerpt shows a large match on the Lua value variant: Nil, Boolean, Integer, Number, String, Table, and UserData all get explicit arms, with the UserData arm itself containing a nested match on TypeId to dispatch between UrlBuf, PathBufDyn, Id, and AnyData. Below the main match, an inventory iteration loop handles plugin-registered types via DataInventory.

A CC of 55 with a fan-out of only 3 tells you the complexity is almost entirely from branching — 55 independent execution paths through what reads as a large, flat match expression. The exit_heavy pattern confirms that many branches return early, each of which is a coverage requirement.

With 0 touches in the last 30 days and last modified 43 days ago, this function is sitting still — not urgent for this week. But the next time any engineer needs to add a new Lua type or extend the Data enum, they’ll be navigating 55 branching paths in a single function. That’s where regressions happen: not because the function is being actively churned, but because the next change lands somewhere too large to reason about safely. The blast radius of a mistake here extends to every Lua-to-Rust type conversion in the plugin layer.

Recommendation: Decompose by type category. The primitive conversions (Nil, Boolean, Integer, Number, String) can move to a primitive_value_to_data helper. The Table logic — which already recurses via value_to_key — is independently testable and should be its own function. The UserData dispatch is a third natural extraction. That restructuring would leave value_to_data as a coordinator of three helpers, each with a CC well below 20.


run — main.rs

run
yazi-cli/src/main.rs
13.41
fire
CC 39
ND 2
FO 5
touches/30d 1

run is the async top-level dispatcher for the ya CLI binary. The source excerpt shows a large match over Args::parse().command, with arms for Emit, EmitTo, Exec, Pkg, Pub, PubTo, Sub, Cache, and likely more. Each arm initializes subsystems (yazi_boot::init_default, yazi_dds::init, yazi_tty::init, yazi_config::init), dispatches to the appropriate command handler, and manages error output with std::process::exit(1) on failure.

Cyclomatic Complexity 39
threshold: 10

A CC of 39 in a CLI entry point is not unusual in isolation — command dispatchers accumulate branches. But combined with the exit_heavy and long_function patterns, it means this function carries a large test surface: each command arm is a distinct execution path, and the error-handling branches within each arm multiply that count further. The fan-out of 5 is modest given the scope, but the commit history is the most notable external signal in the top five — one in four commits to this file has been tagged as a bug fix (1 of 4 total commits). That’s a meaningful historical signal for a function that routes all CLI traffic.

With 1 commit in the last 30 days (13 days ago), this is live code under active modification. Any new ya subcommand will land here and extend the CC further. The shallow nesting depth of 2 is a silver lining — the function is wide, not deep, so the branching is at least readable even if it’s not testable.

Recommendation: Apply the standard CLI dispatch refactoring: move each command arm into its own handle_<command> async function, leaving run as a thin router that matches on the command variant and delegates. That immediately reduces the CC of run substantially and makes each command’s initialization and error-handling logic independently testable. Given the elevated bug-fix rate, I’d prioritize the Emit/EmitTo/Pub/PubTo arms first — they share a structural pattern and are the most likely source of the historical fixes.


What else caught my attention

Three functions from the broader dataset are worth a brief mention. valid_wtf8 in yazi-shim/src/wtf8/validator.rs carries a CC of 37 and sits in the debt quadrant — last modified 109 days ago with 0 touches in the last 30. That’s the most dormant high-complexity function in the context data, and a clear candidate for the debt queue. from_str in yazi-config/src/keymap/key.rs has a CC of 89, also debt-quadrant, with 0 touches in 54 days — extreme structural debt that will be painful when keymap parsing next needs to change. Neither is actively changing right now, but both carry high blast radius when they eventually do. The try_absolute_impl function in yazi-fs/src/engine/local/absolute.rs sits just below the top five with an activity-weighted risk of 13.39 and 2 touches in the last 30 days — a fire-quadrant function worth including in any near-term review sweep.

Patterns Found

Antipatterns detected across the top functions in this snapshot:

PatternOccurrences
exit_heavy8
complex_branching4
deeply_nested1
long_function1
hub_function1

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, hub_function, long_function.

Reproduce This Analysis

git clone https://github.com/sxyazi/yazi
cd yazi
git checkout caa7797eb9e344a478b3a9f8772e0202328aeb0f
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.

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