Skip to content

fix: stop reporting reachable C# code as unreachable - #735

Open
SirCrest wants to merge 1 commit into
peteromallet:mainfrom
SirCrest:fix/csharp-unreachable-false-positives
Open

SirCrest wants to merge 1 commit into
peteromallet:mainfrom
SirCrest:fix/csharp-unreachable-false-positives

Conversation

@SirCrest

@SirCrest SirCrest commented Sep 8, 2026

Copy link
Copy Markdown

Problem

detect_unreachable_code reports reachable C# as dead code. On a 211k-LOC
C# codebase it produced 7 unreachable_code findings and all 7 were false:

Unreachable code after yield_statement   Sussudio/Services/Runtime/FfmpegRuntimeLocator.cs:114
Unreachable code after yield_statement   Sussudio/Services/Runtime/FfmpegRuntimeLocator.cs:123
Unreachable code after yield_statement   tools/Common/PresentMon/PresentMonProbe.cs:278
Unreachable code after yield_statement   tools/Common/PresentMon/PresentMonProbe.cs:279
Unreachable code after return_statement  Sussudio/Services/Runtime/FfmpegRuntimeLocator.cs:202
Unreachable code after return_statement  Sussudio/Services/Runtime/NativeFfmpegCapabilityProbe.cs:335
Unreachable code after return_statement  Sussudio/Services/Audio/WasapiAudioCapture.cs:907

Three distinct causes:

  1. yield_statement was in _TERMINATOR_TYPES, with a comment conceding it
    is "not strictly terminating, but often last in generators". Consecutive
    yield return statements in an iterator are all reachable — each resumes when
    the caller pulls the next element — so any iterator with more than one yield
    in a block got flagged.

  2. C# local functions are hoisted. return result; followed by
    void Check(int code, string op) { ... } is idiomatic C#, but the declaration
    after the return was reported as unreachable.

  3. Nodes inside a tree-sitter ERROR region were reported. Where the parser
    loses sync the tree no longer describes real control flow; the
    WasapiAudioCapture.cs:907 hit above is an ERROR node whose text is just }.

Fix

  • Drop yield_statement from _TERMINATOR_TYPES. It is not a terminator in any
    supported language (Python yields parse as expression_statement, JS as
    yield_expression), so this only removes the C# false positives.
  • Add _HOISTED_DECLARATION_TYPES = {"local_function_statement"} and skip those
    when scanning past a terminator.
  • Skip ERROR nodes when reporting.

After the fix those same four files report [].

Tests

Four regression tests added to TestUnreachableCode:

  • test_csharp_local_function_after_return_is_reachable
  • test_csharp_consecutive_yields_are_reachable
  • test_error_nodes_are_not_reported_as_unreachable
  • test_csharp_statement_after_return_is_still_flagged — pins that genuine dead
    code after a return is still reported, so this does not weaken the detector.

python -m pytest desloppify/tests/lang/ -q → 603 passed. The two failures in
test_treesitter_imports_direct.py and the 34 in the full suite are present on
an unmodified checkout of main and are unrelated to this change.

🤖 Generated with Claude Code

detect_unreachable_code produced three classes of false positive on C#:

1. `yield_statement` was listed as a control-flow terminator, with a
   comment conceding it is "not strictly terminating". Consecutive
   `yield return` statements in an iterator are all reachable - each
   resumes when the caller pulls the next element - so any iterator with
   more than one yield in a block was flagged.

2. C# local functions are hoisted, so `return result; void Check(...) {}`
   is idiomatic rather than dead code, but the declaration following the
   return was reported.

3. Nodes inside a tree-sitter ERROR region were reported. Where the
   parser lost sync the tree no longer describes real control flow, so a
   "statement" after a terminator there is a parse artifact.

On a 211k-LOC C# codebase these accounted for every unreachable_code
finding: 7 reported, 7 false.

Real dead code after a return is still reported; a regression test pins
that alongside the three fixes.
@awdemos

awdemos commented Sep 12, 2026

Copy link
Copy Markdown

Reviewed against main — the bugs are real (yield_statement in _TERMINATOR_TYPES, local_function_statement/ERROR flagged after terminators) and the removal is correctly scoped (only C# emits yield_statement among the installed grammars). One correctness issue to fix first:

Hoisted local functions launder genuinely dead code. The new "don't flag hoisted declarations" logic lives inside the saw_terminator branch, so the hoisted declaration consumes the one flagged slot and resets the terminator. Verified empirically: return; void Check() {}; Console.WriteLine("dead"); now produces zero findings — the WriteLine is unreachable and should be flagged.

Your own (self-closed) #725 handled this correctly: skip hoisted declarations via continue before the terminator check so the terminator stays pending, with an explicit test (test_statement_after_hoisted_declaration_is_still_unreachable) proving dead code after the local function is still flagged. Please port that skip structure into this PR — then the "does not weaken the detector" claim holds.

Two smaller items: #725 also dropped the terminator across ERROR nodes (the conservative choice in a known-broken parse region); consider keeping that. And the pin test here only covers dead code with no local function in between, so it doesn't currently prove the no-weakening claim.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants