Skip to content

fix(matching): attribute coverage to generic and overloaded methods - #4

Open
pavelsem wants to merge 1 commit into
7Factor:mainfrom
pavelsem:fix/generic-and-overload-coverage-matching
Open

fix(matching): attribute coverage to generic and overloaded methods#4
pavelsem wants to merge 1 commit into
7Factor:mainfrom
pavelsem:fix/generic-and-overload-coverage-matching

Conversation

@pavelsem

@pavelsem pavelsem commented Sep 7, 2026

Copy link
Copy Markdown

A generic method and an overload set both scored 0.0 coverage however well tested, because the two canonical keys could not be equal:

Roslyn MyApp.Helper.TryConvertToNumeric<>(string, out T)
Cobertura MyApp.Helper.TryConvertToNumeric(string, ref T)

Three independent mismatches, each of which alone is enough to lose the match:

  1. Method-level generic arity. Roslyn knows the method is generic and writes Foo<>; a Cobertura usually carries no arity, so neither the exact pass nor the name-only fallback can match. The fallback strips the signature but keeps the arity marker.
  2. Parameter modifiers. Roslyn writes out T/in T; the CLR records one by-ref marker, normalized here to ref T.
  3. Nullable-reference annotations. Roslyn writes string?; a CLR signature cannot express it.

2 and 3 only break the exact-signature pass — but that is the only pass able to resolve an overload set, since the name-only fallback rightly refuses multiple candidates. So overloads went unmatched too.

The fix, in two parts:

  • NormalizeSignatureForMatching reduces a signature to what BOTH sides can express: out/in/ref fold to ref, and ? is dropped on both sides (so Roslyn's int? and Cobertura's Nullable still agree). The canonical key is a matching key, not a display name.
  • A new arity-relaxed pass sits between the exact and name-only passes. Arity is deliberately KEPT in the exact key so a real Find/Find<T,U> pair stays distinguishable; the new pass only relaxes it, and only when exactly one candidate remains. Ambiguity declines rather than guessing — inventing coverage is worse than reporting none.

Verified against a production solution's coverlet output (127 methods, one project): 7 methods gained their real coverage, 0 lost any, crappyMethodCount 7 -> 3, totalCrap 1411 -> 711.
TryConvertToNumeric goes 0.00 -> 1.00 and CRAP 552 -> 23 against a Cobertura that always said branch-rate="1" for it. Across that whole solution 51 of 51 crappy generics had read exactly 0.000 — not one generic anywhere carried coverage.

Tests: 223 pass in Core. Four existing assertions changed, each a deliberate contract change with the reason recorded at the test: the two generic-key tests now pass unchanged, Cobertura_NullableType loses its ?, and the overload test that asserted 0.0 was never ambiguous — it is split into the case that now resolves and a genuinely ambiguous one that must still refuse.

Not fixed here: Cli.Tests FullPipeline_MinCrapFilter fails on a clean checkout of b173d10 as well, unrelated to matching.

A generic method and an overload set both scored 0.0 coverage however
well tested, because the two canonical keys could not be equal:

  Roslyn     MyApp.Helper.TryConvertToNumeric<>(string, out T)
  Cobertura  MyApp.Helper.TryConvertToNumeric(string, ref T)

Three independent mismatches, each of which alone is enough to lose the
match:

1. Method-level generic arity. Roslyn knows the method is generic and
   writes Foo<>; a Cobertura <method name> usually carries no arity, so
   neither the exact pass nor the name-only fallback can match. The
   fallback strips the signature but keeps the arity marker.
2. Parameter modifiers. Roslyn writes `out T`/`in T`; the CLR records one
   by-ref marker, normalized here to `ref T`.
3. Nullable-reference annotations. Roslyn writes `string?`; a CLR
   signature cannot express it.

2 and 3 only break the exact-signature pass — but that is the only pass
able to resolve an overload set, since the name-only fallback rightly
refuses multiple candidates. So overloads went unmatched too.

The fix, in two parts:

* NormalizeSignatureForMatching reduces a signature to what BOTH sides
  can express: out/in/ref fold to ref, and `?` is dropped on both sides
  (so Roslyn's `int?` and Cobertura's Nullable<int> still agree). The
  canonical key is a matching key, not a display name.
* A new arity-relaxed pass sits between the exact and name-only passes.
  Arity is deliberately KEPT in the exact key so a real Find<T>/Find<T,U>
  pair stays distinguishable; the new pass only relaxes it, and only when
  exactly one candidate remains. Ambiguity declines rather than guessing —
  inventing coverage is worse than reporting none.

Verified against a production solution's coverlet output (127 methods,
one project): 7 methods gained their real coverage, 0 lost any,
crappyMethodCount 7 -> 3, totalCrap 1411 -> 711.
TryConvertToNumeric<T> goes 0.00 -> 1.00 and CRAP 552 -> 23 against a
Cobertura that always said branch-rate="1" for it. Across that whole
solution 51 of 51 crappy generics had read exactly 0.000 — not one
generic anywhere carried coverage.

Tests: 223 pass in Core. Four existing assertions changed, each a
deliberate contract change with the reason recorded at the test:
the two generic-key tests now pass unchanged, Cobertura_NullableType
loses its `?`, and the overload test that asserted 0.0 was never
ambiguous — it is split into the case that now resolves and a genuinely
ambiguous one that must still refuse.

Not fixed here: Cli.Tests FullPipeline_MinCrapFilter fails on a clean
checkout of b173d10 as well, unrelated to matching.
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.

1 participant