Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
c648361
Fix post-release CRAP completeness, async grouping and exclusion meta…
ANcpLua Oct 13, 2026
511ea1e
Respect exclusions and non-executable declarations in CRAP completeness
ANcpLua Oct 13, 2026
84f449d
Fix ambiguous exclusion accounting and prepare DotCov 3.0.1
ANcpLua Oct 14, 2026
fc66e14
Pin patched cryptography dependencies required by NuGet audit
ANcpLua Oct 14, 2026
dfbe8f1
Require unique executable metrics matches for coverage completeness
ANcpLua Oct 14, 2026
854bbd9
Align metrics matching documentation with completeness semantics
ANcpLua Oct 14, 2026
4fe9109
Reject incompatible metrics arities while preserving extension receivers
ANcpLua Oct 14, 2026
aaa9fbd
Avoid redundant lifted-null branches in arity compatibility
ANcpLua Oct 14, 2026
de457b8
Deduplicate excluded metrics identities before matching candidates
ANcpLua Oct 14, 2026
7aaf360
Pre-filter zero-body metrics from the exclusion index
ANcpLua Oct 14, 2026
2b43679
Index metrics summaries by arity and parse extension displays once
ANcpLua Oct 14, 2026
c39ba62
Preserve indexer accessor arity for metrics completeness
ANcpLua Oct 14, 2026
5312277
Close remaining metrics scope and complexity review findings
ANcpLua Oct 14, 2026
911a493
Keep source-aware exclusions from consuming retained metrics siblings
ANcpLua Oct 14, 2026
8fb50af
Record the 3.0.1 corrective patch in the rolling changelog
ANcpLua Oct 14, 2026
fe1c30b
Assert conservative gate outcome for receiver-ambiguous extension met…
ANcpLua Oct 14, 2026
c1c5b0a
Honor legacy Roslyn executable-line metrics without overriding modern…
ANcpLua Oct 14, 2026
a0b38bc
Match complete overload sets by indexed parameter signatures
ANcpLua Oct 14, 2026
151ef17
Import coverage fixture helpers for signature regressions
ANcpLua Oct 14, 2026
2212689
fix: reconcile indexer, nullable, generic type and constructor identi…
ANcpLua Oct 14, 2026
87d1f52
fix: bound metrics identity work and prevent cross-method complexity …
ANcpLua Oct 14, 2026
73ad088
test: align nullable type identity assertion with TUnit constraints
ANcpLua Oct 14, 2026
f4ca7ca
fix: fold top-level entry point and async body into one measured method
ANcpLua Oct 14, 2026
177badb
fix: require one unique measured constructor pair per type
ANcpLua Oct 14, 2026
e3aed78
fix: enforce metrics source and signature identity while preserving g…
ANcpLua Oct 14, 2026
fa5fb35
test: cover scoped metrics fallback and legacy orphan identities
ANcpLua Oct 14, 2026
4b2472c
ci: print uncovered branch locations when the coverage gate fails
ANcpLua Oct 14, 2026
8fcb4de
ci: recognize Coverlet branch diagnostics independently of boolean ca…
ANcpLua Oct 14, 2026
5c33f20
test: cover explicit empty roots and orphan legacy properties
ANcpLua Oct 14, 2026
d17e239
fix: normalize real metrics type signatures and rooted exclusions
ANcpLua Oct 14, 2026
136e285
style: use required constant patterns in tuple normalization
ANcpLua Oct 14, 2026
831c764
test: use CLI error contract for oversized signatures
ANcpLua Oct 14, 2026
22cc490
fix: preserve nullable suffix removal during signature normalization
ANcpLua Oct 14, 2026
a47762c
fix: preserve reference identities and input context in metrics matching
ANcpLua Oct 14, 2026
ae837df
fix: reconcile nullable and file-local identities conservatively
ANcpLua Oct 14, 2026
2f5f4f4
style: mark nullable identity selectors static
ANcpLua Oct 14, 2026
242aed9
fix: keep malformed nullable signatures conflicting
ANcpLua Oct 14, 2026
d95a97a
Close the last CRAP findings and run the dotcov test gate cases on Wi…
ANcpLua Oct 14, 2026
11da5dc
Apply the complexity floor in analysis and treat IL parentheses as un…
ANcpLua Oct 14, 2026
10a5a6b
Match async overload bodies and chosen roots; keep function pointers …
ANcpLua Oct 14, 2026
9fc0b07
Keep invalid metrics rows unmatched, name malformed members, read Vis…
ANcpLua Oct 14, 2026
4fed7c5
Keep rows without complexity, pair source-less rows with one file, re…
ANcpLua Oct 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/badge/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ inputs:
default: 'true'
version:
description: dotcov.tool version to install.
default: '3.0.0'
default: '3.0.1'
command:
description: >-
Command that runs dotcov instead of installing it, split on whitespace, e.g.
Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/nuget-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,21 @@ jobs:
run: |
dotnet tool restore
dotnet fallout Coverage --reports TestResults
- name: Locate uncovered branches after a failed coverage gate
if: failure() && matrix.os == 'ubuntu-latest'
run: |
python3 - <<'PY'
from pathlib import Path
import xml.etree.ElementTree as ET

for report in Path('TestResults').glob('**/*cobertura*.xml'):
print(f"Coverage diagnostics: {report}")
for source in ET.parse(report).findall('.//class'):
for line in source.findall('./lines/line'):
coverage = line.get('condition-coverage', '')
if coverage and not coverage.startswith('100%'):
print(f"{source.get('filename')}:{line.get('number')}: {coverage}")
PY

# Exercise the composite action and its default installer against this checkout's portable
# package. A release candidate is not on NuGet yet. Native packages are packed separately;
Expand Down
73 changes: 39 additions & 34 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,6 @@

Rolling task-session log, oldest → newest, capped at 20 entries.

## Task 1 — 2026-05-15 — Coverlet-cased `branch="True"`, branch dedup, 100% self-coverage

Built on top of `b306779` (which switched `ParseClass` to a full `<class>` subtree walk
with per-source-line Math.Max aggregation across multiple IL-type class blocks).

Changed:
- Case-insensitive match for `branch="true"` / `branch="True"` in `CoberturaParser`. The
previous literal-pattern compare rejected Coverlet's Pascal-cased value (`XmlConvert
.ToString(bool)`), silently dropping every branch line in Coverlet output — branch counts
rendered as a fake 100% because `TotalBranches` was 0.
- New `BranchLinesSeen` set on `LineAccumulator` so branches are counted once per source
line. Without this, lines that appear both under `<methods>/<method>/<lines>` and
`<class>/<lines>` would double-count their branch totals.
- `CoverageDiff.Compare` switch reordered to remove the unreachable `_ => throw` default.
- `Ansi.EnableOnWindows` marked `[ExcludeFromCodeCoverage]` — its two branches can only be
exercised on different OSes; the "never throws" contract test still covers it.
- Added `coverlet.runsettings` excluding `[CompilerGenerated]` and `[GeneratedCode]` types
and `obj/` source-generator output, so async-state-machine phantom branches don't cap us.

Verified:
- `dotnet test --collect:"XPlat Code Coverage" --settings coverlet.runsettings` followed by
`dotcov report TestResults/ --exclude-generated` reports **Lines 100%** /
**Branches 100%** across 165 tests.
- Regression tests cover the methods+class-level duplication (with `branch="True"`
branches that previously double-counted), case-insensitive `branch` attr matching across
`true`/`True`/`TRUE`, missing/malformed `hits`, and Removed/Added file rendering across
all three formatters (Table, Markdown, JSON).

Notes:
- Numbering starts at 1; no prior CHANGELOG existed in repo metadata.
- The pre-existing `97.9% line / 100% branch` figure in commit `ee3bf32` was an artefact
of the casing bug — `branch="True"` lines were ignored, so total branches was 0 and the
rate trivially rendered as 100%.

## Task 2 — 2026-05-15 — Codecov-pattern alignment: branch dedup, strict line state, indirect-change diff

Lifted three semantically-valuable patterns from Codecov's coverage model and grafted them
Expand Down Expand Up @@ -839,3 +805,42 @@ Verified:
fails on `5a50ddf` and passes here; each policy has a contract test per command through the
CLI entry point and through the Fallout target (`FalloutBuildTests`, `RepositoryBuildTests`);
`dotcov test` contracts run against a stand-in dotnet (`FakeDotnet`).

## Task 21 — 2026-10-14 — 3.0.1: CRAP completeness and exclusion corrections

- Missing executable metrics members prevent PASS; ambiguous overloads and known arity
mismatches remain unmatched. Indexer accessors retain index and setter-value counts.
- Metrics preserve executable-line counts and source paths. Non-executable declarations
neither require coverage nor provide executable complexity. Exclusions apply to
metrics-only files, preserve ordered keep rules, and never consume retained siblings.
- Async bodies retain their own identity in either input order. Merged exclusion metadata
lists only absent files. Indexed metrics matching avoids repeated candidate scans.
- Patched Fallout cryptography dependencies to 10.0.13 without suppressing NuGet audit.
- A metrics complexity below 1 on an executable member supplies no complexity (reader input
and library callers alike) and the row still counts as unmatched, so the gate is NODATA.
The reader keeps rows with a missing or malformed complexity as 0 instead of dropping them.
- Visual Basic metrics displays (`Sub`/`Function … As T`, generic `M(Of T)`, `Sub New`,
`ByRef`, `Optional`, `ParamArray`, `(Of T)`, `T()`) are normalized for matching while the
raw display is kept for listings; tuple syntax is validated there too, so a
malformed member names the metrics file and member.
- Parentheses in a Cobertura signature (only a truncated function pointer produces them)
make it unknown instead of an error; malformed tuple syntax stays an input error in
metrics displays, where source tuple syntax really occurs.
- Coverage signatures with a function pointer (Coverlet truncates them to `(System.Int32))`)
and metrics `delegate*` parameters are unknown signatures; `crap` no longer throws
`Unbalanced tuple type`. Escaped identifiers (`@int`) drop the escape after aliasing;
a generic `T?` that is a value type in one method and a reference type in another stays
unresolved instead of being forced to `Nullable<T>` report-wide.
- Metrics `delegate*` parameters lower to one canonical identity (calling convention dropped,
`void` aliased to `System.Void`), so they conflict with other known signatures while
Coverlet's truncated spelling stays unknown.
- State-machine-only async overloads consume their metrics rows when the body and row counts
of a member in one file agree (rows without a source file only when one file holds such
bodies); `--source-root` now also roots relative metrics paths.
- `GateAwareTableWiringTests` use the shared `FakeDotnet`, so their two `dotcov test` cases
run on Windows too; one Linux/macOS-only test remains (Unix file modes).
- Aligned package/action/README versions and rewrote `docs/releases/3.0.1.md` around
user-visible behaviour.

Validation: each fix has a regression test that fails before it; PR #79's CI is the
full-suite and 100/100 coverage record.
13 changes: 6 additions & 7 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

<!-- Versioning: CI passes -p:Version=x.y.z -->
<PropertyGroup>
<VersionPrefix Condition="'$(VersionPrefix)' == ''">3.0.0</VersionPrefix>
<VersionPrefix Condition="'$(VersionPrefix)' == ''">3.0.1</VersionPrefix>
<VersionSuffix Condition="'$(VersionSuffix)' == ''">local</VersionSuffix>
<Version Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix)</Version>
<Version Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)</Version>
Expand All @@ -29,12 +29,11 @@
<!-- The release's contract changes first, then the fixes with the direction each number moves;
docs/releases/$(VersionPrefix).md holds the full notes. -->
<PackageReleaseNotes>
DotCov 3.0.0 changes three gate contracts; every number a gate decides on may move.
1. Thresholds are explicit: check, test, the Fallout target and the badge action have no default --min-line 80. Pass --min-line, --min-branch, or both (0 to 100); without either the command exits 2 before reading a report.
2. The CRAP gate needs every method scored: one unscored method makes it NODATA (exit 3). Emitters without a usable complexity attribute need --metrics.
3. File identity is deterministic: several source roots no longer trigger a disk probe; unresolved files make check, crap and test NODATA until --probe-sources or --source-root.
Fixes: contradictory condition detail and empty class summaries no longer inflate branch and line rates; condition identity no longer depends on XML packaging; CRAP rows stay per file and per overload, raising diluted scores; ./src/C.cs and src/C.cs count once; ParseAsync finishes method-only classes.
New: --exclude-test-code. Full notes: https://github.com/ANcpLua/dotcov/blob/main/docs/releases/$(VersionPrefix).md
DotCov 3.0.1 fixes CRAP passes that 3.0.0 could not prove; line and branch gates are unchanged.
CRAP: an unmeasured metrics method, an ambiguous overload match or a complexity below 1 now gives NODATA (exit 3); async overloads keep their own body, so diluted scores rise; state-machine-only async overloads match their metrics rows; function-pointer parameters no longer abort crap; declarations without a body need no coverage; --source-root also roots relative metrics paths; Visual Basic metrics displays match.
Exclusions: excluded methods count as measured without a score and never hide a kept sibling; merged reports no longer list a re-added file as excluded.
DotCov.Fallout requires System.Security.Cryptography.Pkcs/Xml 10.0.13.
Full notes: https://github.com/ANcpLua/dotcov/blob/main/docs/releases/$(VersionPrefix).md
</PackageReleaseNotes>
</PropertyGroup>

Expand Down
5 changes: 5 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<Project>

<PropertyGroup>
<!-- Pin Fallout's transitive cryptography dependencies to the patched versions.
NuGet also promotes these constraints when packing DotCov.Fallout. -->
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
<TUnitVersion>1.74.0</TUnitVersion>
<CoverletMtpVersion>10.1.0</CoverletMtpVersion>
<FalloutVersion>10.4.0</FalloutVersion>
Expand All @@ -13,6 +16,8 @@
<PackageVersion Include="Fallout.Common" Version="$(FalloutVersion)" />
<PackageVersion Include="Fallout.Components" Version="$(FalloutVersion)" />
<PackageVersion Include="ANcpLua.Analyzers" Version="$(ANcpLuaAnalyzersVersion)" />
<PackageVersion Include="System.Security.Cryptography.Pkcs" Version="10.0.13" />
<PackageVersion Include="System.Security.Cryptography.Xml" Version="10.0.13" />
</ItemGroup>

</Project>
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ permissions:

steps:
# … checkout, setup-dotnet, and a test run writing Cobertura to TestResults/
- uses: ANcpLua/dotcov/.github/actions/badge@v3.0.0
- uses: ANcpLua/dotcov/.github/actions/badge@v3.0.1
with:
reports: TestResults
min-line: 80
Expand Down Expand Up @@ -249,5 +249,5 @@ fallout Coverage # tests plus the coverage gate
## Feedback

[Issues](https://github.com/ANcpLua/dotcov/issues) ·
[Release notes](docs/releases/3.0.0.md) ·
[Release notes](docs/releases/3.0.1.md) ·
[MIT](LICENSE)
62 changes: 62 additions & 0 deletions docs/releases/3.0.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# DotCov 3.0.1

A patch for the 3.0 CRAP gate and for exclusions. Line and branch gates are unchanged. Some CRAP
gates that passed on 3.0.0 now return `NODATA` (exit 3) or `FAIL` (exit 1); in each case 3.0.0
reported a pass it could not prove.

## CRAP gate

- **Every metrics method must be measured.** A method or accessor in the `--metrics` file with no
coverage counterpart makes the gate `NODATA`. 3.0.0 listed it and still passed.
- **Each metrics row matches exactly one method.** Overloads match by parameter types, including
tuples, nullable value and reference types, generics, indexers, `ref readonly`, escaped
identifiers such as `@int`, and file-local types. A row that matches several methods stays
unmatched, so the gate says `NODATA` instead of lending one method's complexity to a sibling.
- **Declarations without a body need no coverage.** Members with `ExecutableLines=0` (interface
and abstract members) neither block a pass nor supply complexity.
- **A complexity below 1 is not a measurement.** A metrics row whose `CyclomaticComplexity` is
0, negative, malformed or missing supplies no complexity and still counts as unmatched, so the
gate says `NODATA`; 3.0.0 scored it 0 or dropped the row and could pass.
- **Visual Basic metrics match.** `Sub A.M(value As Integer)`, `Function A.M(...) As Long`,
generic `M(Of T)` methods and `Sub New` constructors, with `ByRef`, `Optional`, `ParamArray`,
`List(Of T)` and `T()` parameters, match their IL signatures like C# displays.
- **Async overloads keep their own body.** When a lambda preceded its state machine in the
report, 3.0.0 could fold an async overload into a covered synchronous one and pass. CRAP
scores for such methods rise to their own values.
- **Async overloads measured only through their state machines are matched.** When a file holds
nothing but the state machines of a member's overloads and the metrics file has exactly as
many rows for it, the rows pair with the bodies; each body is scored with the larger complexity.
Rows without a source file pair only when a single file holds such bodies.
- **Function-pointer parameters no longer abort `crap`.** Coverlet truncates their signature;
3.0.0 failed with `Unbalanced tuple type`. Such methods now match by name and parameter count,
and a metrics `delegate*` row never matches a method with other parameter types.
- **`--source-root` also roots relative metrics paths**, so files resolved against the chosen
root find their metrics rows.
- **Excluded files stay accounted for.** Methods removed by `--exclude-*` count as measured
without being scored, and an excluded overload never hides a kept sibling.
- Conversion operators that differ only in their return type stay `NODATA`: Cobertura omits the
return type, so their coverage cannot be told apart.

## Reports

- A merged report no longer lists a file as excluded when another input brings it back.
- Large reports with many excluded files or large metrics files no longer scan quadratically.
Coverage signatures and metrics names longer than 16,384 characters are rejected as input
errors (exit 5); tuple nesting is limited to 64 levels and metrics type nesting to 256. A
malformed metrics member is reported with the metrics file and the member's name.

## Packages

- `DotCov.Fallout` requires `System.Security.Cryptography.Pkcs` and `System.Security.Cryptography.Xml`
10.0.13, which address the advisories of October 13, 2026.
- `CodeMetricsMember` gains nullable `ExecutableLines`, `File` and `TypeIdentity`. CLI flags, exit
codes and threshold rules are unchanged from 3.0.0.

## Upgrade

```bash
dotnet tool update -g DotCov.Tool --version 3.0.1
```

Use `ANcpLua/dotcov/.github/actions/badge@v3.0.1` and `DotCov`/`DotCov.Fallout` 3.0.1. The 3.0
threshold and file-identity rules in the [3.0.0 notes](3.0.0.md) still apply.
3 changes: 2 additions & 1 deletion src/DotCov.Tool/DotCovCli.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ public static async Task<int> RunAsync(string[] args, TextWriter stdout, TextWri
}

static bool IsExpectedFailure(Exception ex) =>
ex is UsageError or CliError or UndeclaredSourceRootException or XmlException or IOException or UnauthorizedAccessException;
ex is UsageError or CliError or UndeclaredSourceRootException or XmlException or IOException or
InvalidDataException or UnauthorizedAccessException;

// Not async: each arm hands back its command's Task, so a synchronous command's exception
// still surfaces inside RunAsync's try, and the switch stays one branch per command.
Expand Down
18 changes: 18 additions & 0 deletions src/DotCov/BalancedText.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,22 @@ internal static int CountTopLevelItems(ReadOnlySpan<char> list)
}
return count;
}

internal static IEnumerable<string> SplitTopLevel(string list)
{
if (list.Trim().Length is 0) yield break;
var depth = 0;
var start = 0;
for (var i = 0; i < list.Length; i++)
{
if (depth is 0 && list[i] is ',')
{
yield return list[start..i].Trim();
start = i + 1;
}
if (list[i] is '"' or '\'') i = LiteralEnd(list, i);
else depth += DepthDelta(list[i]);
}
yield return list[start..].Trim();
}
}
13 changes: 9 additions & 4 deletions src/DotCov/CoberturaParser.Methods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public static MethodCoverageReport ParseMethods(Stream stream, long maxChars = D
using (var reader = CreateReader(stream, maxChars, async: false))
CollectMethods(reader, methods, scope, sourceName: null);
scope.RequireDeclaredRoot();
return methods.Materialize();
return methods.Materialize(identity?.SourceRoot);
}

/// <summary>
Expand Down Expand Up @@ -61,10 +61,14 @@ public static MethodCoverageReport ParseMethods(
{
throw new ReportParseException(input.SourceName, ex);
}
catch (InvalidDataException ex)
{
throw new InvalidDataException($"{input.SourceName}: {ex.Message}", ex);
}
}

scope.RequireDeclaredRoot();
return methods.Materialize();
return methods.Materialize(identity?.SourceRoot);
}

/// <summary>Walk one document into <paramref name="methods"/>; roots are per document, warnings accumulate.</summary>
Expand Down Expand Up @@ -115,14 +119,15 @@ public void Complete(DocumentContext document)
_sourceRoots.Add(root);
}

public MethodCoverageReport Materialize()
public MethodCoverageReport Materialize(string? sourceRoot)
{
var result = new List<MethodCoverage>(_order.Count);
foreach (var entry in _order)
result.Add(entry.Materialize());
return new MethodCoverageReport(result, _warnings, _sourceRoots)
{
AmbiguousFiles = AmbiguousFile.Union(_ambiguous)
AmbiguousFiles = AmbiguousFile.Union(_ambiguous),
SelectedSourceRoot = sourceRoot
};
}

Expand Down
3 changes: 3 additions & 0 deletions src/DotCov/CoberturaParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,9 @@ public void Visit(XmlReader sub)

private void BeginMethod(XmlReader sub)
{
if (methods is not null)
MethodSignatures.ValidateLength(sub.GetAttribute("signature") ?? "",
$"Coverage signature for {className}.{sub.GetAttribute("name")} in {file}");
_methodAcc = methods?.For(
new MethodKey(file, className, sub.GetAttribute("name") ?? "", sub.GetAttribute("signature") ?? ""),
DecodeComplexity(sub.GetAttribute("complexity")));
Expand Down
Loading
Loading