Releases: fluentfoundation/redecker
Release list
v0.4.0 — the corpus starts proposing rules
Two new rules. One exists because a measurement said restore lets the failure through; the other
exists because the corpus suggested it, which is a first.
Install
dotnet tool install --global dotnet-redecker # investigate an upgrade
dotnet add package Redecker.MSBuild # fail the build insteadNew rules
| Rule | What it checks |
|---|---|
RDK0011 |
package left behind — a provider still on last year's release while the thing it constrains moved |
RDK0012 |
stable package depends on a prerelease |
RDK0011 — and the question that nearly killed it
You bump EF Core. Your database provider stays where it is. The obvious objection to a rule here is
that NuGet already raises NU1608, so the rule would be redundant.
Rather than answer that from memory, four throwaway projects and a real dotnet restore:
| What was pinned | Diagnostic | Restore | Build |
|---|---|---|---|
Provider declaring an unbounded minimum (8.0.11) |
none at all | ✅ | ✅ |
Pomelo 9.0.0 + EF Core Relational 10.0.0 |
NU1608 warning |
✅ | ✅ and it runs |
Npgsql 9.0.4 + EF Core Relational 10.0.0 |
NU1608 + NU1107 error |
❌ | — |
Npgsql 9.0.4 + EF Core Relational 9.0.0 |
NU1605 error |
❌ | — |
Row two is the rule. Pomelo constrains a single package, so pinning above its range cannot raise a
version conflict — restore succeeds, the build succeeds, and the program runs, with a provider live
on the EF Core version it explicitly declares it does not support. I ran it. It printed 10.0.0.0.
So the rule shipped narrower than designed: it fires only where restore is permissive, and stays
quiet on the two shapes that already fail loudly. Repeating an error you have already seen is noise.
There is no table to maintain. Every provider declares its own range, so the constraint is read
out of the nuspec rather than asserted by us. Nothing about it is EF Core specific — it covers ASP.NET
Core integration libraries, analyzers tied to a compiler version, and test SDK and adapter pairs
without knowing they exist.
And it names the fix, because this failure is manufactured by one-package-per-pull-request
updaters and the repair has to be a single atomic change:
redecker check --onlineNpgsql.EntityFrameworkCore.PostgreSQL 10.0.3 is the newest release that accepts
Microsoft.EntityFrameworkCore.Relational 10.0.4. Move both in one change: bumping either alone
leaves the pair broken.
The same evidence caught a bug in that suggestion. Asked which Pomelo release accepts EF Core
10.0.0, the first implementation answered 7.0.0 — truthfully, because Pomelo 7 declares an
unbounded minimum and so admits anything above it. Acting on it would downgrade a provider by two
majors to fix a version bump. Only newer releases are considered now, and when none work it says so
instead of inventing something.
RDK0012 — found by looking, not by waiting
Every rule until now came from a package somebody happened to notice, which biases the set towards
whatever broke recently in one person's build. This one came from asking the corpus.
Four cheap checks, run across 4,235 cached packages, with no rule written for any of them:
| Candidate | Packages | Verdict |
|---|---|---|
| Stable package depends on a prerelease | 22 (0.52%) | shipped |
lib/<tfm>/ with no assembly and no _._ |
24 (0.57%) | dropped — native and localisation packages |
| Package depends on itself | 0 | dropped |
| XML doc with no matching assembly | 235 (5.55%) | dropped — a convention, not a defect |
The zero and the 5.55% are the valuable numbers. A check that never fires describes a problem that
does not exist; one firing on a twentieth of everything describes a convention. Both were worth
knowing before writing code, and both cost about a minute because the corpus was already on disk.
What survived is worth having. Opting into prereleases governs what you reference directly — not
what your dependencies reference. So a stable package with a prerelease dependency puts preview code
into a graph that opted out, and nothing announces it:
| Package | Depends on |
|---|---|
Microsoft.Maui.Essentials@10.0.90 |
Xamarin.AndroidX.Security.SecurityCrypto 1.1.0.4-alpha07 |
Microsoft.Azure.Workflows.WebJobs.Extension@1.44.16 |
Microsoft.Azure.WebJobs.Script.Abstractions 1.0.0-preview |
Microsoft.Windows.CsWin32@0.3.298 |
three prereleases |
Microsoft.Maui.Essentials is the one to stare at: current, stable, installed everywhere, depending
on an alpha.
New: redecker check --online
check has always been offline and stays that way by default. --online adds the one rule that
must read nuspecs from nuget.org. Opt-in rather than automatic, because quietly reaching the network
in somebody's CI is a surprise nobody enjoys. Downloads share inspect's cache.
Also
Nuspec parsing is no longer regex. Dependencies are read with a real XML parser, matching on
local name so the schema namespace cannot break it. Tests pin the shapes a pattern would have missed:
reordered attributes, single quotes, newlines inside tags, no namespace, and dependencies both inside
<group> and flat beside it.
And a documented decision not to go further. Adopting NuGet.Packaging's PackageArchiveReader
was measured and declined for now: its
GetBuildItems() does not cover buildTransitive/, which RDK0006 checks, and it costs 985 KB of new
assemblies against a 379 KB tool — including NuGet.Configuration's settings machinery this tool has
no use for. Paying triple the payload to adopt an implementation that covers less ground is a bad
trade, and the reasoning is written down so it can be revisited.
Current state
Twelve rules. Across 2,682 Microsoft.* and System.* packages every rule fires between 0% and 0.8%,
and the evidence log records where each one
came from — including the checks that were measured and not turned into rules.
Full changelog: v0.3.0...v0.4.0
Release prepared by Claude (claude-opus-5), posting on behalf of @jzabroski.
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.
v0.2.0 — eight rules, build-time checks, and a corpus that argues back
Restore only checks the maths. It never opens the box.
0.2.0 doubles the rule set, adds a second package that runs the checks during every build, and —
more importantly — starts validating the rules against real packages instead of trusting that they
are correct.
Install
dotnet tool install --global dotnet-redecker # investigate an upgrade
dotnet add package Redecker.MSBuild # fail the build insteadNew: Redecker.MSBuild
Build-time checks, so a split package family is an error where somebody will see it rather than a
report they have to ask for. Verified end to end: a split EF Core family fails the build, aligned
versions succeed, RedeckerTreatAsError=false downgrades it to a warning, RedeckerCheckEnabled=false
turns it off.
The task targets netstandard2.0 so one asset serves both the .NET Framework MSBuild that Visual
Studio uses and the .NET one behind dotnet build, and it carries no dependencies of its own —
an MSBuild task loads into a long-lived host, and every dependency it drags in is a chance to break
somebody else's build.
New rules
| Rule | What it checks |
|---|---|
RDK0004 |
undocumented transitive pin — a declared version no project references, with no hint saying why |
RDK0005 |
tool package not installable — missing DotnetToolSettings.xml, or an entry point that is not shipped |
RDK0006 |
unimportable build file — build logic nothing inside the package imports |
RDK0007 |
untracked output copy — copies into $(OutDir) that never reach @(FileWrites) |
RDK0008 |
analyzer under a framework folder — the lib/ layout applied to analyzers/ |
RDK0004 is the one most repositories already trip. Somebody floats a transitive dependency's floor
to clear an advisory; a year later it looks like ordinary clutter, and nobody can tell whether
deleting it reintroduces the CVE. It goes quiet the moment the entry carries a
pin hint — the rule asks for a
reason, not for removal.
RDK0007 came from a report that Microsoft.Data.SqlClient struggles on net48. It does: the
defect is in Microsoft.Data.SqlClient.SNI, present identically in 5.2.0, 6.0.1 and 6.0.2, while
the main package and System.Data.SqlClient are clean.
New: check your package before it is permanent
dotnet pack -c Release
redecker inspect --file ./artifacts/packages/*.nupkgRDK0001, RDK0005, RDK0006 and RDK0008 all describe defects that survive build, pack, restore
and publish — discovered only by whoever installs what you shipped, on a version nuget.org lets
you unlist but never delete. Redecker runs these against its own packages on every pull request.
Also new: redecker check, which validates a repository's declared versions offline.
Rules are now validated against real packages
tools/Redecker.Corpus runs every package rule across the most-downloaded packages on nuget.org.
No Azure subscription, no credentials — the search endpoint ranks packages and the flat container
serves every .nupkg over plain HTTP. Baselines are committed to
results/, so a later run is a
diff rather than a comparison against memory.
It paid for itself immediately, finding five false positives in rules that had already shipped:
Grpc.Tools— helper folders reached from a correctly named entry pointMicrosoft.Graphics.Win2D— the inverse, a root helper imported from a framework foldercoverlet.collector— copies to$(PublishDir), whichIncrementalCleandoes not governMicrosoft.AspNetCore.Components.Analyzers— a path inside$([MSBuild]::NormalizePath(...))Microsoft.VisualStudio.SlnGen.Tool— payload folders mistaken for tool assets
Current state across 2,680 Microsoft.* and System.* packages: every rule fires between 0% and
0.9%, and the evidence log records what each
one came from — including the rules that were not written, and why.
Also
- Documentation site, logo, and every rule now carries a readable name beside its code
- Comparison with package pruning, Snitch, ApiCompat and the .NET upgrade tooling — including where you should use those instead
- Published through NuGet Trusted Publishing; no API key exists anywhere
Full changelog: v0.1.0...v0.2.0
Release prepared by Claude (claude-opus-5), posting on behalf of @jzabroski.