Skip to content

--exclude-filter removes all components for any .sln/.slnx/.slnf input, and for single-project input without --recursive #1126

Description

@jasonlhills

Summary

Passing any value to --exclude-filter — including a package name that doesn't exist in the dependency graph — causes the entire component list to be wiped out ("Found 0 packages") in two situations:

  1. Any solution input (.sln, .slnx, .slnf), with or without --recursive (the flag has no effect here — see root cause).
  2. A single project file (.csproj etc.) without --recursive.

It works correctly only for: a single project file with --recursive

Reproduced on v6.2.0 (latest); traced back to the commit that introduced --exclude-filter (#939, 1615122, 2025-04-27) — present in every release since.

Repro

(1) Broken: solution filter, --recursive has no effect either way

dotnet-CycloneDX MySolution.slnf --disable-package-restore --exclude-filter "PackageThatDoesNotExist" --filename bom.json
dotnet-CycloneDX MySolution.slnf --recursive --disable-package-restore --exclude-filter "PackageThatDoesNotExist" --filename bom.json

(2) Broken: single project, no --recursive

dotnet-CycloneDX MyProject.csproj --disable-package-restore --exclude-filter "PackageThatDoesNotExist" --filename bom.json

(3) Works: single project, with --recursive

dotnet-CycloneDX MyProject.csproj --recursive --disable-package-restore --exclude-filter "PackageThatDoesNotExist" --filename bom.json

Cases (1) and (2) print, for every restored package:

The following orphaned packages have been removed:
-
Found 0 packages

Case (3) correctly prints No orphaned packages were found. and produces a full component list.

Root cause

ExcludeFilterHelper.RemoveOrphanedPackages (ExcludeFilterHelper.cs) does a BFS starting only from packages with IsDirectReference == true, then intersects the package set with whatever's reachable:

var queue = new Queue(packages.Where(p => p.IsDirectReference));
...
packages.IntersectWith(reachablePackages);

This runs in Runner.cs right after packages are loaded:

if (!string.IsNullOrEmpty(options.DependencyExcludeFilter))
{
ExcludeFilterHelper.ExcludePackages(packages, options.DependencyExcludeFilter);
ExcludeFilterHelper.RemoveOrphanedPackages(packages); // ~line 251
}

But at this point in the flow, IsDirectReference has not been populated for most input types:

  • ProjectFileService.GetProjectDotnetDependencysAsync (the method used for a plain single project without --recursive, and also used internally, per-project, by the solution path below) never sets IsDirectReference on anything.
  • ProjectFileService.RecursivelyGetProjectDotnetDependencysAsync (used only for a single project with --recursive) does set it immediately on load — this is the one path that works.
  • For .sln/.slnx/.slnf input, Runner.cs branches to SolutionFileService.GetSolutionDotnetDependencys before ever checking --recursive (Runner.cs:171-182), so the flag has no effect for solutions at all. That method does contain logic that looks like it should propagate direct-reference
    status per project:

// SolutionFileService.cs:185-199
var projectPackages = await _projectFileService.GetProjectDotnetDependencysAsync(...);
directReferencePackages.UnionWith(projectPackages.Where(p => p.IsDirectReference));
...
foreach (var directPackage in directReferencePackages)
{
packages.TryGetValue(directPackage, out var package);
package.IsDirectReference = true;
}

  • but since GetProjectDotnetDependencysAsync never sets IsDirectReference in the first place, directReferencePackages is always empty and this block is effectively dead code.
  • The only place that actually populates IsDirectReference for these paths is Runner.cs's RemoveProjectReferencesAndMakeTheirDependenciesDirect(packages) call — but that runs after the exclude-filter block (~line 344 vs ~line 251).

So for every input type except "single project + --recursive", the BFS starts from an empty set, and the filter step deletes everything.

Suggested fix

Ensure IsDirectReference is populated before the exclude-filter block runs, for all input types — e.g. by moving/duplicating the RemoveProjectReferencesAndMakeTheirDependenciesDirect call (or equivalent) earlier in Runner.cs, and/or fixing GetProjectDotnetDependencysAsync to set the flag
on load so SolutionFileService's existing propagation logic actually does something.

Environment

  • cyclonedx-dotnet (CycloneDX global tool) 6.2.0+55877e2ae058ae9686783ac084d2257d3fcedab1
  • .NET SDK 10.0.301
  • Reproduced against a single leaf .csproj, a single .csproj with --recursive, and a multi-project .slnf

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    triageDon't know what to do with this yet

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions