Repository navigation
Add command to find redundant package references and version conflicts in NuGet - #282
Open
ChrisonSimtian wants to merge 2 commits into
Open
ChrisonSimtian wants to merge 2 commits into
ChrisonSimtian wants to merge 2 commits into
Conversation
dennisdoomen
force-pushed
the
feature/dependency-hygiene-analyzer
branch
from
October 11, 2026 06:39
e8d307c to
d1c6551
Compare
| /// <summary> | ||
| /// Gets the lowest version the project asked for, normalized from its version range. | ||
| /// </summary> | ||
| public string DeclaredVersion { get; init; } = ""; |
| NuGetVersion? highestTransitive = target.Libraries | ||
| .SelectMany(library => library.Dependencies) | ||
| .Where(edge => string.Equals(edge.Id, dependency.Name, StringComparison.OrdinalIgnoreCase)) | ||
| .Select(edge => edge.VersionRange?.MinVersion) |
| @@ -0,0 +1,193 @@ | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| public void Does_not_report_a_conflict_when_the_frameworks_differ() | ||
| { | ||
| // Arrange | ||
| var api = new LockFileBuilder("Api", "net9.0").WithPackageReference("Serilog", "3.1.1").Build(); |
| @@ -0,0 +1,133 @@ | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| /// <summary> | ||
| /// Gets or sets the logger used to report progress and diagnostics. | ||
| /// </summary> | ||
| public ILogger Logger { get; set; } = NullLogger.Instance; |
| /// <summary> | ||
| /// Gets or sets whether the .NET restore may prompt for input, such as feed credentials. | ||
| /// </summary> | ||
| public bool InteractiveRestore { get; set; } = true; |
| /// <summary> | ||
| /// Gets or sets whether to restore even if the lock file is up-to-date. | ||
| /// </summary> | ||
| public bool ForceRestore { get; set; } |
| /// <summary> | ||
| /// Gets or sets whether to never restore, even if the lock file is missing or out-of-date. | ||
| /// </summary> | ||
| public bool SkipRestore { get; set; } |
| /// <summary> | ||
| /// Gets the names of the direct dependencies that could already provide another package. | ||
| /// </summary> | ||
| public IEnumerable<string> ProviderRoots => |
… conflicts Add DependencyHygieneAnalyzer, which reads the restore graph that DotNetLockFileLoader already loads and reports: - package references that a referenced project or another direct package already provides, and whether removing them keeps the resolved version - packages that resolve to different versions across projects, grouped per target framework A PrivateAssets="all" or auto-referenced package is never treated as a provider, so a shipped dependency is not reported as removable. Expose it as `packageguard dependencies` with --conflicts, --severity, --exclude and the restore options. The command loads lock files through a new LockFileCollector instead of the full policy analysis, so it skips feed and license lookups, the package cache, policy loading and the npm scan. Run the command from the RunPackageGuard build target, without --severity error, so it is exercised but cannot fail the build yet. Co-Authored-By: Chrison Simtian <csimon@chrison.dev> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
dennisdoomen
force-pushed
the
feature/dependency-hygiene-analyzer
branch
from
October 11, 2026 06:48
d1c6551 to
77d2f5f
Compare
When analyze runs with --report-risk, reuse the lock files the scan already loaded to find redundant references and version conflicts, and list them in a separate Dependency Hygiene section. They are not scored, so they never change the risk zones, the overall status or the exit code. SARIF is unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The analysis engine and the
packageguard dependenciescommand for #281. That issue carries the motivation and three open questions; this is the working code behind it, so you can judge the shape rather than the description.Problem
PackageGuard already reads every project's restore graph. The same graph answers two questions it does not ask yet:
PackageReferenceneeded? It is redundant when a referenced project, or another direct package, already brings the same package in.Both are dependency hygiene rather than policy or risk.
Change
Engine.
DependencyHygieneAnalyzerinPackageGuard.Coretakes theNuGet.ProjectModel.LockFilethatDotNetLockFileLoaderalready produces. No new input path, no second restore, no network access.Two rules are worth calling out, because they took a real run to get right:
PrivateAssets="all"orAutoReferencedpackage is never a provider, and is never reported itself. It is not a dependency of what the project ships, so removing another reference because it pulls it in would drop a shipped dependency.Ignores_a_package_that_is_only_there_at_build_timecovers it.Version conflicts are grouped per target framework first, so a solution that resolves differently per framework on purpose is not reported.
Command.
packageguard dependencies [--path <path>] [--conflicts] [--severity error] [--exclude <package>], plus the usual restore options (-i,-f,-s,-v).--conflicts.--severity errorexits with code 1 when a reported finding exists, so the exit code never depends on a finding the output hides. The default,warning, always exits with 0.ProjectAnalyzer. A newLockFileCollectorfinds the projects and loads their lock files, and nothing else. It skips feed and license lookups, the package cache, policy loading and the npm scan.dotnet restoreonly runs whenproject.assets.jsonis missing or out of date.Risk report. With
analyze --report-risk, the same findings appear in the HTML report as a separate Dependency Hygiene section, and as one summary line on the console. They reuse the lock files the scan already loaded, so there is no extra restore or network access. They are not scored: they never change risk scores, zones, the overall status, violations or the exit code. SARIF is unchanged on purpose, because findings there would become code-scanning alerts and redundancy findings have no file location.Docs.
website/docs/dependencies.md, linked fromusage.md,risk-metrics.mdand the README.Self-scan.
RunPackageGuardnow runs the command on this repo. It is informational only, because the command is run without--severity error. It currently reports 24 redundancy findings here, many of them false positives (for exampleJetBrains.AnnotationsinPackageGuard, which the code uses directly). That is the "the graph is not the source code" caveat showing up on our own solution. Making it a gate needs a baseline option or--excludeentries first.Still open from #281
The command name, the
DependencyHygieneAnalyzername, and one finding type versus two are still to be decided. The command layer is small, so a rename is cheap.Tests
New specs for the analyzer (in-memory
LockFilethroughLockFileBuilder, so no restore runs), forLockFileCollector(no restore, project without a lock file) for the command settings (defaults, and which findings are reported), and for the report section (rendering, encoding, and an unchanged risk summary).On this branch, rebased onto
main: 454 specs pass on net9.0 and net10.0, and the API verification test passes.PackageGuard.Corebuilds with zero warnings underTreatWarningsAsErrors.Two notes
AcceptApiChanges.shdoes the wrong thing on .NET 10. Verify writesnet8.0.DotNet10_0.received.txtwhile the approved file isnet8.0.verified.txt, so the script'ss/received/verified/creates a new runtime-specific file and leaves the real one stale. I updatednet8.0.verified.txtby hand. A separate fix would be welcome..csprojfile name. A project whoseAssemblyNamediffers from its file name is not matched, so a redundancy through it is missed. That is a missing finding, never wrong advice, and it is listed under the risks in [API Proposal]: Find redundant package references and version conflicts #281.https://claude.ai/code/session_01TK6dHqkZwhjWUrZNnnb8MG
🤖 Generated with Claude Code