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
- 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.
- 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.
Summary
C# ABC counts a
relational_pattern's operator token and the constructthat 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 forabc.conditions == cyclomatic() - 1per space; these two shapes break it.Measured
At
HEADoffix/issue-1297(the behaviour is unchanged frommain—the pre-#1297 denylist did not name
relational_patterneither, so itsoperator counted then and counts now):
abc.conditionscyclomatic()cyclomatic() - 1p(constant patterns)n(relational patterns)pscores one condition perswitch_expression_arm.nscores the sametwo arms plus the
>and<of each arm'srelational_pattern.The
isform has the same shape:abc.conditions = 2againstcyclomatic() - 1 = 1— one from theIfStatementcondition-slot walker treating theis_pattern_expressionas a boolean terminal, one from the pattern's
>.Where
csharp_count_token_conditioninsrc/metrics/abc/csharp.rs. TheGT | LTallowlist admitsRelationalPatternalongsideBinaryExpression/BinaryExpression2, and theSwitchExpressionArmarm and the
IfStatementcondition slot each count the enclosingconstruct 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 thex is > 0operator to keep counting — the issue's own expected residueis 2, not 1 — so removing
RelationalPatternfrom the allowlist wouldhave been a different, unmeasured change riding along with it.
csharp_relational_pattern_still_counts_as_a_conditioninsrc/metrics/abc.rspins the current value and cross-references thisissue.
Options
RelationalPatternfrom theGT | LTallowlist. Both shapesabove 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.
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
inclauses, Pythoncaseclauses.