Skip to content

v0.3.0 — two rules that had to earn it

Choose a tag to compare

@jzabroski jzabroski released this 31 Jul 23:04

Two new rules. Neither one shipped as designed, and that is the story of this release.

Both were proposed as ideas, measured against thousands of real packages first, and then rewritten
when the corpus disagreed. One of them was wrong three times before it was right.

Install

dotnet tool install --global dotnet-redecker    # investigate an upgrade
dotnet add package Redecker.MSBuild             # fail the build instead

📖 Documentation

New rules

Rule What it checks
RDK0009 incomplete symbol package — a .snupkg that covers some shipped assemblies but not others
RDK0010 framework folder mismatch — an assembly under lib/<framework>/ that a consumer targeting it could not load

RDK0010, and the two rules that didn't work

The idea is simple: lib/net45/ is a promise, and nothing verifies it. Restore reads the folder
name, picks the nearest match, and copies out whatever is inside without ever opening it. A net472
assembly under lib/net45/ restores cleanly and then buries the consumer in errors about types they
never touched.

The obvious implementation — compare the folder against the assembly's TargetFrameworkAttribute,
report any difference — produced 285 findings across 4,205 packages, and most of them were fine.
A netstandard2.0 assembly in lib/net8.0/ is not a mistake; it is the ordinary way to win
nearest-framework matching. A net45 build in lib/net452/ is a build reused rather than repeated.

The question that works is the one restore actually asks: can a project targeting this folder
consume this assembly?
NuGet already answers that, so Redecker asks it instead of re-deriving the
netstandard fallback chain and PCL profile contribution. 285 → 87.

Half of what remained sat in dead platforms, where MonoAndroid403 is an OS version, uap10.0
means .NETCore,Version=v5.0, and a PCL profile is a set intersection rather than a version. Not
wrong — unfixable, because the tooling that would republish those packages is gone. 87 → 42, in
22 packages.

Version of the rule Findings Useful
Compare the strings 285 mostly not
Ask NuGet about compatibility 87 yes, plus dead platforms
Scope to living frameworks 42 yes

What it catches is real, and a lot of it is Microsoft's own:

Package Folder Assembly targets
System.Security.Cryptography.OpenSsl@5.0.0 lib/net461/ .NETFramework,Version=v4.7
Microsoft.VisualStudio.TextTemplating.15.0 lib/net45/ .NETFramework,Version=v4.7.2
Microsoft.Web.Administration@11.1.0 lib/netstandard1.5/ .NETFramework,Version=v4.5
Microsoft.CodeCoverage@18.8.1 lib/net8.0/ .NETFramework,Version=v4.0
Microsoft.VisualStudio.DesignTools.Extensibility lib/net45/ .NETStandard,Version=v2.0

That last one is the trap running backwards. netstandard2.0 needs net461 or later, so a net45
project cannot load it either — being older is not automatically safe, which the compatibility
check gets right for free and a version comparison would have got wrong.

It is a warning rather than an error because Microsoft.CodeCoverage ships that .NET Framework 4.0
shim deliberately, and it is installed in a large share of the test projects in existence. Failing
those builds would destroy more work than it saved.

RDK0009 — incomplete symbol package

Publishing no symbols is a choice. 174 of 232 sampled packages make it, so the rule says nothing
there. Publishing some is almost always an accident: of the 58 that publish symbols, 57 cover
every assembly they ship
.

The single exception taught the rule its one exclusion. Microsoft.VisualStudio.Validation has 26
uncovered assemblies — all satellites, which correctly have no PDBs, because a resource assembly has
no code to step through. Written from the idea alone, that package would have been the rule's first
false positive.

Works offline on your own output, where a finding is still cheap to act on:

dotnet pack -c Release
redecker inspect --file ./artifacts/packages/Contoso.Widgets.1.0.0.nupkg

Surveys, as distinct from sweeps

A sweep runs the shipped rules and asks whether any of them has started lying. A survey runs
a measurement that is not a rule yet, to decide whether it should become one.

dotnet run --project tools/Redecker.Corpus -c Release -- survey-tfm results

Surveys read the download cache rather than the network, which makes them free to re-run — and that
turned out to matter more than it sounds. The target-framework survey was re-run twice because its
classifier was wrong, and being able to throw away an answer cheaply is what made it easy to admit.
Results are committed to
results/.

Fixed

RDK0006 false positives, both found by the top-2000 sweep: build logic reached only through a
property-valued import, and entry points supplied by MSBuild extension-point properties. The rule
went from 15 findings to 4 without losing a true one.

This one is worth naming, because RDK0006 had been retired outright at one
point
on the grounds that the corpus said it
misfired. That was the wrong call — testing shows the presence of bugs, not their absence, and a
rule that fires wrongly is usually a rule with a bug rather than a bad idea. It was restored and
then fixed.

Under the hood

RDK0010 reads PE metadata through System.Reflection.Metadata, which is in the shared framework
and, unlike Assembly.LoadFrom, neither runs code nor cares whether the host process could load the
assembly — reading a net40 image from a net8.0 tool has to work, because that mismatch is the
entire point.

New dependency: NuGet.Frameworks, for the compatibility question above. It is dependency-free
and it is the first step of what
issue #5 argues for more broadly: stop
hand-rolling what NuGet already implements correctly.

Current state

Ten rules. Across 2,682 Microsoft.* and System.* packages every rule fires between 0% and 0.8%,
and the evidence log records what each one
came from — including the rules that were not written, and why.

Full changelog: v0.2.0...v0.3.0


Release prepared by Claude (claude-opus-5), posting on behalf of @jzabroski.