Skip to content

fix(abc/csharp): relational pattern operator double-counts against its arm #1383

Description

@dekobon

Summary

C# ABC counts a relational_pattern's operator token and the construct
that owns the pattern, so a relational pattern scores twice what the
equivalent constant pattern scores and twice what C#'s own cyclomatic
decision count says. .claude/rules/grammar-dispatch.md §8 asks for
abc.conditions == cyclomatic() - 1 per space; these two shapes break it.

Measured

At HEAD of fix/issue-1297 (the behaviour is unchanged from main
the pre-#1297 denylist did not name relational_pattern either, so its
operator counted then and counts now):

class A {
    int p(int x) => x switch { 5 => 1, 0 => 2, _ => 3 };
    int n(int x) => x switch { > 5 => 1, < 0 => 2, _ => 3 };
}
method abc.conditions cyclomatic() cyclomatic() - 1
p (constant patterns) 2 3 2 ✓
n (relational patterns) 4 3 2 ✗

p scores one condition per switch_expression_arm. n scores the same
two arms plus the > and < of each arm's relational_pattern.

The is form has the same shape:

class A { int m(int x) { if (x is > 0) { return 1; } return 0; } }

abc.conditions = 2 against cyclomatic() - 1 = 1 — one from the
IfStatement condition-slot walker treating the is_pattern_expression
as a boolean terminal, one from the pattern's >.

Where

csharp_count_token_condition in src/metrics/abc/csharp.rs. The
GT | LT allowlist admits RelationalPattern alongside
BinaryExpression / BinaryExpression2, and the SwitchExpressionArm
arm and the IfStatement condition slot each count the enclosing
construct independently.

Why it was not fixed in #1297

#1297 is about non-comparison < / > (operator < declarations,
JSX delimiters, Lua attributes, super<A>). Its C# row expects the
x is > 0 operator to keep counting — the issue's own expected residue
is 2, not 1 — so removing RelationalPattern from the allowlist would
have been a different, unmeasured change riding along with it.
csharp_relational_pattern_still_counts_as_a_condition in
src/metrics/abc.rs pins the current value and cross-references this
issue.

Options

  1. Drop RelationalPattern from the GT | LT allowlist. Both shapes
    above then land on the cyclomatic decision count (2 and 1
    respectively), because the enclosing arm / condition slot already
    pays for the decision. This is the §8-consistent answer and matches
    how a constant pattern is treated.
  2. Keep counting the operator but stop counting the enclosing construct
    when its pattern is relational. Harder to justify — the arm is the
    decision.

Option 1 looks right, but it changes a published metric for every C#
file using pattern matching, so it wants its own measurement pass over
a real corpus before it lands.

Sibling sweep

Check whether any other language double-counts a pattern's comparison
operator against the arm that owns it — Kotlin when (x) { in 1..2 -> },
Rust match guards, Ruby in clauses, Python case clauses.

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