Conversation
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.
|
Review findings are now in the line-specific requested-changes review. CI diagnosis: the latest build passed restore, build, tests, and pack. It failed at |
| // key above rather than stripped there, so a genuine Find<T>/Find<T,U> pair stays | ||
| // distinguishable; this pass only relaxes it, and only when the result is unique. | ||
| var arityKey = StripArity(fullKey); | ||
| if (arityKeyLookup.TryGetValue(arityKey, out var arityMatches) && arityMatches.Count == 1) |
There was a problem hiding this comment.
With source Foo(int) and Foo<T>(int) and one arityless Foo(int) coverage entry, the nongeneric method matches exactly. This condition then matches the generic method to the same entry. Please reject a relaxed match when multiple source methods share its key, and test this case.
There was a problem hiding this comment.
Fixed in 6952d40. The relaxed pass now also requires exactly one source method with the relaxed key. Test: RelaxedMatch_SharedBySeveralSourceMethods_Refuses.
The name-only pass has the same gap: it counts coverage entries, but not source methods. This behavior existed before this PR, so I suggest a separate issue for it.
goneflyin
left a comment
There was a problem hiding this comment.
The added code comments need an editing pass. If available, please use Scott's $precise-technical-writing skill (the code-comment guidance) or a similar standard: state the behavior or invariant briefly, keep problem history in the PR description, and remove claims the code does not enforce. Examples are attached to the relevant lines.
| continue; | ||
| } | ||
|
|
||
| // Pass 1b: Retry with the method's generic arity dropped. Roslyn always knows a |
There was a problem hiding this comment.
This comment retells the PR rationale and says the relaxed match is unique, but the current check counts only coverage entries. After fixing that ambiguity, one or two lines stating when the fallback is safe would suffice. Please remove the emphasis on EVERY.
There was a problem hiding this comment.
Fixed in 6952d40. The comment now states the match condition in two lines. EVERY is removed.
| /// Reduce a normalized signature to the information both sides can actually carry. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Two kinds of detail exist on the Roslyn side and nowhere in a CLR signature, and each |
There was a problem hiding this comment.
This remarks block repeats the defect history and describes int? becoming int as safe, although those are distinct value-type overloads (see the earlier review). After correcting that behavior, please reduce the documentation to the matching contract: which distinctions the key preserves and which it discards.
There was a problem hiding this comment.
Fixed in 6952d40. The remarks now state the contract: out and in fold to ref, and ? is kept.
| namespace Crap4DotNet.Core.Tests.Matching; | ||
|
|
||
| /// <summary> | ||
| /// Coverage is silently discarded for two shapes that are ordinary in real code: |
There was a problem hiding this comment.
This class header repeats the PR description. The claim that every case is taken verbatim from production is also hard to verify from the constructed MyApp fixtures. Please keep only context the test names and inputs cannot show; a short sentence describing the test scope is enough.
There was a problem hiding this comment.
Fixed in 6952d40. The header is one sentence, and the verbatim claim is removed.
| [Fact] | ||
| public void ArityRelaxedMatch_AmbiguousCandidates_Refuses() | ||
| { | ||
| // The arity-relaxed pass exists because a Cobertura method name usually carries no |
There was a problem hiding this comment.
These comment blocks explain the test history and matcher implementation at length. The fixture and assertion already show most of that. Please keep only the nonobvious setup, such as: Neither coverage entry matches Find<T> exactly; both share its arity-stripped key. The earlier overload test comment could be trimmed the same way.
There was a problem hiding this comment.
Fixed in 6952d40. The comments now contain only the nonobvious setup. The fixture comment uses your suggested wording, and the earlier overload test comment is one line.
|
Hi @pavelsem -- sorry for the slow reply, we have a Pavel here at @7Factor and I presumed you were him erroneously. Thanks for the PR -- first one on this! Mostly good, a couple things in the code that are identified. As for the comments -- AI does a lousy job at knowing when to say more, and when to say less. Those are examples of it -- happens all the time. My approach is to use this skill I created, but it's just my take and probably not much better than telling it to use ASD STE-100. |
…ared relaxed keys - The exact key keeps `?`, so Foo(int) and Foo(int?) no longer share a key. - The relaxed pass also drops `?`. It matches only when exactly one coverage entry and exactly one source method have the relaxed key. - Shorten code comments to the matching contract. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Hi @goneflyin - Thanks for your suggestions and thorough review. The changes are implemented, tests added and passing. Can you please have a look again? |
|
Follow-up: #12 tracks the two limits listed in the PR description. They are the name-only pass gap and the Coverlet entry that combines |
Generic methods and some overloaded methods receive coverage 0.0, even when the Cobertura file reports coverage for them.
Cause
The Roslyn key contains three details that the Cobertura key does not contain:
Foo<>. Coverlet writesFoo.out Torin T. The CLR signature has one by-ref marker, normalized toref T.string?. The CLR signature hasstring.Each detail prevents the exact-key match. The name-only fallback cannot recover these methods: its key keeps
<>, and it rejects a name with more than one coverage entry (an overload set).Change
NormalizeSignatureForMatchingfoldsoutandintoref. It keeps?, soFoo(int)andFoo(int?)keep distinct keys.?annotations. The pass matches only when exactly one coverage entry and exactly one source method have the relaxed key. Otherwise the method continues to the name-only pass.Matching order: exact key, relaxed key, name-only key.
Known limits (not changed here)
Foo(int)andFoo<T>(int)in the same class. The entry contains the lines of both methods.Foo(int)receives the combined coverage, andFoo<T>(int)receives 0.0. Evidence: a local coverlet run produced one<method name="Foo" signature="(System.Int32)">entry with lines from both methods.Foo(int)andFoo(string)and one coverage entryFoo(System.Int32), both methods receive that entry's coverage. This behavior existed before this PR.Tests
NullableValueTypeOverloads_KeepTheirOwnCoverage,RelaxedMatch_SharedBySeveralSourceMethods_Refuses, and the cases inGenericAndOverloadMatchingTests.FallbackMatch_OverloadedMethods_NoFallbackis renamed toOverloadedMethods_ResolvedByTheExactSignaturePassand now expects 0.7, becauseout intandSystem.Int32&produce the same key.ArityRelaxedMatch_AmbiguousCandidates_Refusescovers the ambiguous case.FullPipeline_MinCrapFilterfails. It also fails on a clean checkout of b173d10, so the failure is not related to matching.Production check
Measured on 52656cd against one project of a production solution (127 methods). Not re-measured on 6952d40.
TryConvertToNumeric<T>: coverage 0.00 → 1.00, CRAP 552 → 23. Cobertura reportsbranch-rate="1"for this method.Related to #7.
🤖 Generated with Claude Code