v0.3.0 — two rules that had to earn it
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 insteadNew 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.nupkgSurveys, 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 resultsSurveys 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.