Skip to content

fix(csharp): a when guard on a switch arm is a branch neither metric counts #1422

Description

@dekobon

Summary

A when guard on a C# switch arm is a real branch — the pattern can match
while the guard fails — and neither metric models it. C# cyclomatic has
no when_clause arm, and C# ABC has no guard rule. ABC nonetheless appears
to count some guards, because its token arms fire on whatever operator
happens to sit inside one. The result is that the score depends on how the
guard is spelled rather than on what it does.

Found while fixing #1383,
which deliberately left the guard alone.

Measured

At HEAD of fix/batch-2026-09-10 (after #1383):

class A {
    static bool IsEven(int x) { return true; }
    int tok(int x) => x switch { > 0 when x % 2 == 0 => 1, > 0 => 2, _ => 3 };
    int cmp(int x) => x switch { > 0 when x > 2      => 1, > 0 => 2, _ => 3 };
    int call(int x) => x switch { > 0 when IsEven(x) => 1, > 0 => 2, _ => 3 };
}
guard abc.conditions cyclomatic() cyclomatic() - 1
when x % 2 == 0 3 3 2
when x > 2 3 3 2
when IsEven(x) 2 3 2

Three semantically equivalent guards, two different ABC scores. The ==
and > are counted by csharp_count_token_condition's EQEQ | BANGEQ and
GT | LT arms respectively; IsEven(x) contains no such token, so it
contributes nothing.

Cyclomatic is uniform at 3 across all three — it counts the two non-discard
arms and the base, and never the guard.

Two defects, one fixture

  1. Cyclomatic under-counts. A guarded arm has two exits (pattern fails;
    pattern matches and guard fails) but contributes one decision. Compare
    Rust and Python, which do count a match guard — measured as part of
    fix(abc/csharp): relational pattern operator double-counts against its arm #1383's sibling sweep, match n { x if x > 5 => 1, _ => 0 } scores
    cyclomatic 3, and C#'s equivalent scores 2. So C# is the outlier among
    the languages here, not the convention.
  2. ABC is spelling-dependent. Whatever is decided for (1), ABC should
    score a guard by what it is, not by whether its body happens to contain a
    token an unrelated arm matches.

Where

  • src/metrics/cyclomatic/csharp.rs — counts SwitchExpressionArm (and the
    statement switch's Case), with no when_clause arm.
  • src/metrics/abc/csharp.rs, csharp_count_token_condition — the token
    arms that incidentally fire inside a guard.

csharp_switch_arm_guard_operator_still_counts in src/metrics/abc.rs
pins the current behaviour and cross-references this issue, so a fix here
must update that test deliberately rather than discovering it.

Note for whoever takes this

.claude/rules/grammar-dispatch.md §8 asks for
abc.conditions == cyclomatic() - 1 per space, and the temptation will be
to reach it by suppressing ABC's guard token. That is the wrong direction:
it would make tok and cmp agree with a cyclomatic count that is itself
too low. Fix cyclomatic first, then re-derive what ABC should say.

Both changes move published metrics for C#, so each wants a corpus pass.
Be aware the DeepSpeech corpus contains no switch expressions at all
(established in #1383), so it cannot measure either.


Resolution

Status: Fixed (pending merge)
Commit: eb4468c (+ d08eb8e) on branch fix/batch-2026-09-13

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions