Across 817 analyzed functions in Aider-AI/aider, 203 score at the critical band — and the five that rank highest have one thing in common: none of them has been touched in 66 days. That makes this a structural-debt story rather than a live-churn story. I would start with send_message in aider/coders/base_coder.py — an activity-weighted risk score of 17.55, cyclomatic complexity of 99, and fan-out to 47 distinct callees make it the single highest blast-radius function in the repository when the next change lands. The repo’s 417 debt-quadrant functions (versus only 4 in fire) tell me that the dominant risk here is accumulated complexity waiting to be disturbed, not code that is actively regressing right now.
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 |
|---|---|---|---|---|---|
get_testimonials_js | scripts/homepage.py | 19.2 | 54 | 11 | 21 |
send_message | aider/coders/base_coder.py | 17.6 | 99 | 5 | 47 |
get_input | aider/io.py | 17.3 | 74 | 5 | 36 |
_parse_update_file_sections | aider/coders/patch_coder.py | 16.9 | 43 | 6 | 16 |
main | benchmark/benchmark.py | 16.9 | 66 | 4 | 54 |
Large Repo Analysis
aider 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 risk profile
817 functions analyzed
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.Exit Heavy×6Exit Heavy
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.God Function×6God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.Complex Branching×5Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.Deeply Nested×4Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.
The quadrant picture is stark: 417 functions in the debt quadrant, 4 in fire. That asymmetry means the dominant risk in aider right now is not regression from active churn — it is structural complexity that has been sitting untouched long enough that the next developer to open any of these files will encounter significant cognitive overhead with no recent context to lean on. The top five all share the same 66 days since last change, so the clock on all of them has been running equally long.
The antipattern distribution reinforces this. Seven long functions, six each of exit-heavy and god-function, five with complex branching, and four with deep nesting — these patterns cluster together and compound each other. A god function that is also exit-heavy requires a reader to track both a large number of dependencies and multiple early-return paths simultaneously.
get_testimonials_js — scripts/homepage.py
This is the top-ranked function by activity-weighted risk score (19.17), which is initially surprising given its location in a homepage script. From the source excerpt, it reads README.md, locates a “Kind Words From Users” section by scanning line by line, then parses each testimonial entry through a branching chain of string-split operations that handles four distinct author/link format variants: em-dash with link, regular dash with link, em-dash without link, and regular dash without link. Each branch further subdivides on whether a URL is present.
That parsing logic produces a cyclomatic complexity of 54 and a maximum nesting depth of 11 — the deepest nesting in the top five. ND 11 means a reader must track eleven layers of nested conditionals simultaneously to reason about any single code path through the innermost branches. The fan-out of 21 is notable for a script that looks, by name, like it should be simple text extraction; it suggests the function is doing considerably more orchestration than its description implies, making it a god function in practice.
The function hasn’t been touched in 66 days and has zero commits in the last 30 days. Worth noting: the single recorded commit on this file was a bug fix — too small a sample to draw conclusions about defect history, but worth keeping in mind.
My recommendation here is extract-method refactoring targeted at the four author/link format parsers. Each format variant is a natural extraction candidate — pulling them into named helper functions (e.g. _parse_em_dash_author_with_link, _parse_dash_author_plain) would cut the nesting depth substantially and make each path independently testable. The current structure requires exercising eleven nesting levels to reach the innermost branches, which the exit-heavy pattern (multiple break and return points throughout) makes even harder to cover with tests.
send_message — aider/coders/base_coder.py
This is the function I would treat as the most consequential structural risk in the repository. A cyclomatic complexity of 99 means 99 independent execution paths — each one a required test case and a potential bug surface. The fan-out of 47 distinct callees is the second-highest in the top five, and in Python specifically, those 47 calls involve duck-typed interfaces where the actual types are resolved at runtime, making the true coupling broader than the number suggests.
From the source excerpt, send_message handles the full lifecycle of an LLM request: formatting messages, checking token budgets, warming cache, managing streaming versus non-streaming display, running a retry loop over LiteLLM exceptions with exponential backoff, handling keyboard interruptions, catching context-window exhaustion, and managing multi-response continuation when the model hits output length limits. That is at least six distinct concerns encoded into a single generator function.
The exit-heavy pattern is visible in the excerpt: multiple break statements inside nested try/except/while blocks, each representing a different terminal condition. Combined with CC 99, this means the function’s control flow branches nearly a hundred ways before reaching any exit. The god-function pattern is equally apparent — send_message coordinates IO, model communication, error classification, retry scheduling, and streaming display state, all from a single method.
The review comment density of 3.875 on this file is the highest in the top five, which tells me reviewers have historically had a lot to say about it. That is a useful signal: it suggests this function’s complexity has attracted scrutiny before, even if no reverts or bug-linked commits appear in the record.
The function hasn’t been touched in 66 days. When it next is — whether to add a new model’s error class, adjust retry behavior, or handle a new streaming edge case — the developer making that change will need to mentally simulate 99 execution paths through 47 callees. My concrete recommendation is to extract the retry loop into a standalone _send_with_retry method and pull the streaming/display setup into a _prepare_display_state helper. Either extraction alone would materially reduce the CC and make the retry logic independently testable without spinning up the full message pipeline.
get_input — aider/io.py
With a cyclomatic complexity of 74 and fan-out to 36 callees, get_input in aider/io.py is the interactive input handler for the aider REPL. The source excerpt shows it doing a substantial amount of work before a user even types their first character: formatting the file list for display, constructing the prompt prefix from edit format and multiline mode flags, instantiating a ThreadedCompleter wrapping an AutoCompleter, and then registering at least five keybinding handlers as nested closures — Ctrl-Z for background suspension, Ctrl-Space, Ctrl-Up, Ctrl-Down, and Ctrl-X Ctrl-E for external editor invocation.
Those nested closure definitions contribute to both the nesting depth and the exit-heavy pattern. Each keybinding is an anonymous function defined inside get_input, and the overall function has to coordinate all of their shared state. The complex-branching pattern — visible in the conditional logic around multiline_mode, edit_format, abs_read_only_fnames, and the display formatting — accounts for a meaningful portion of the CC 74.
This function hasn’t been touched in 66 days. Its review comment density of 1.0 is modest, suggesting it hasn’t attracted as much review scrutiny as send_message, even though its structural complexity is nearly as high. I would extract the keybinding registration block into a dedicated _build_keybindings method that takes the application state it needs as parameters. That alone would remove several nested closures from get_input’s body, reducing both the CC and the long-function footprint, and would make the keybinding logic independently readable and testable.
_parse_update_file_sections — aider/coders/patch_coder.py
The leading underscore signals this is an internal method, not part of any public API — but its complexity makes it a significant maintenance liability nonetheless. From the source excerpt, it parses structured patch hunks: it reads @@ scope lines to locate a position in the original file, attempts exact matching, falls back to fuzzy whitespace-normalized matching, accumulates a fuzz counter, then delegates to peek_next_section and find_context for the actual diff application.
The dual-pass scope matching — exact then fuzzy — is where the deeply-nested pattern concentrates. A cyclomatic complexity of 43 across a maximum nesting depth of 6 means the innermost match-checking loops sit inside multiple layers of conditional logic: while index < len(lines), conditional on scope line presence, while temp_index < len(orig_lines), a for i, scope in enumerate(scope_lines) inner loop, and the match/break logic. The exit-heavy pattern is visible in the multiple break points that terminate the scope search, the early raise DiffError when scope matching fails, and the terminator checks at the top of the outer loop.
This function hasn’t been touched in 66 days. Patch application logic is notoriously edge-case-dense — the fuzz counter and the comment in the excerpt acknowledging that scope handling could be more robust are both signals that this function has known limitations. I would extract the scope-finding logic (exact and fuzzy passes) into a _find_scope_index helper that returns the matched index and fuzz count, making it independently testable with targeted unit tests for each matching variant without needing to construct full patch inputs.
main — benchmark/benchmark.py
The main function in benchmark/benchmark.py has the highest fan-out of any function in the top five: 54 distinct callees. As the entry point for aider’s benchmarking harness, this is structurally expected — benchmark runners tend to orchestrate many subsystems — but a fan-out of 54 in Python means a change here can produce unexpected ripple effects across more than fifty call sites, and the duck-typed nature of those calls makes the dependency graph implicit rather than explicit.
The source excerpt shows the function accepting roughly twenty Typer CLI parameters — model name, sleep interval, language filters, edit format, editor model, replay mode, keyword filters, clean/continue/new flags, test execution flags, verbosity, statistics modes, diff mode, retry count, thread count, test count, context window override, model settings file, reasoning effort, thinking tokens, and exercises directory. Each parameter introduces at least one additional branch in the execution logic, which is the primary driver of the CC 66 despite a relatively modest max nesting depth of 4.
The review comment density of 3.0 is the second-highest in the top five, suggesting reviewers have had substantive feedback on this file historically. The function hasn’t been touched in 66 days.
Benchmark main functions are a common place where scope accumulates gradually — one new CLI flag per feature, one new conditional per benchmark mode — until the function becomes the authoritative source of truth for the entire tool’s behavior. My recommendation is to decompose along the existing mode boundaries that are visible in the excerpt: stats_only, diffs_only, and the normal execution path are natural candidates for extraction into _run_stats, _run_diffs, and _run_benchmark functions, each accepting a configuration object built from the CLI parameters rather than receiving the full parameter set directly.
Context: the fire quadrant
While the top five are all structural debt, the context_only data surfaces four functions in the fire quadrant — actively changing right now. apply_generic_model_settings in aider/models.py carries a cyclomatic complexity of 100 and has been touched 5 times in the last 30 days, with an activity-weighted risk score of 13.58. configure_model_settings in the same file has 1 touch in the same period, with an activity-weighted risk score of 10.44. Both sit in a file that is clearly under active development — the model configuration layer is being actively iterated, and that context makes the CC 100 in apply_generic_model_settings a live regression risk worth monitoring even though it doesn’t crack the top five by activity_risk. I would flag it as next in queue after the debt functions are addressed.
Patterns Found
Antipatterns detected across the top functions in this snapshot:
| Pattern | Occurrences |
|---|---|
long_function | 7 |
exit_heavy | 6 |
god_function | 6 |
complex_branching | 5 |
deeply_nested | 4 |
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/Aider-AI/aider
cd aider
git checkout 5dc9490bb35f9729ef2c95d00a19ccd30c26339c
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 →