Exclude optional dependencies that no enabled feature activates - #877
Draft
nissessenap wants to merge 3 commits into
Draft
nissessenap wants to merge 3 commits into
nissessenap wants to merge 3 commits into
Conversation
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 53 |
| Duplication | 4 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
`cargo metadata` lists optional dependencies in `resolve.nodes[].deps` whether or not the resolver enabled them, and nothing read the feature information that comes back alongside them. Every optional dependency of every crate in the graph therefore ended up in the SBOM, along with everything reachable only through it: a crate whose sole dependency is `serde_with 3` got 41 components where `cargo tree` reports 13. Replay the feature resolution cargo already recorded. `packages[].features` expands the enabled features of a node into the set of dependencies they activate, following `dep:foo`, `foo/bar`, weak `foo?/bar` and plain feature names. Each edge is matched back to the `[dependencies]` entries that could have produced it - by package name, kind and target platform - and survives if any of them is non-optional or activated. Renamed dependencies are keyed on the rename, since that is what feature syntax refers to. Pruning propagates for free: `all_dependencies` already drops packages its walk does not reach. `index_dep_kinds` gets the same check so an edge that is never built cannot raise a component's scope. Every lookup fails open - an edge whose manifest entry cannot be identified is kept - so this can remove false positives but not introduce false negatives. Verified against `cargo tree -e normal,build` on serde_with, sqlx, reqwest (also under `--target`), rustls, tokio, axum, clap, polars and image: no dependency cargo reports is missing from the output. Workspace feature unification (rust-lang/cargo#7754) and target-conditional features (CycloneDX#871) are unaffected; both are upstream in cargo and neither is made worse. Fixes CycloneDX#766 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Edvin Norling <edvin.norling@kognic.com>
The activation check ran per workspace member, per edge, per dep kind, and each call linearly scanned the parent's `[dependencies]` with a semver comparison per candidate - `filtered_dependencies` twice per node and `index_dep_kinds` re-walking the graph for every member. Whether cargo builds an edge does not depend on which member is being processed, so work it out once for the whole graph instead: `ActivationMap` becomes `BuiltEdges`, a set of `(parent, child, kind)`, and the call sites drop to a hash lookup. `packages` and the nested manifest lookups fall out of `index_dep_kinds`, `filtered_dependencies` and `add_filtered_dependencies`. `is_built` also narrows its candidate manifest entries by the name cargo uses for the edge - the rename, or the library target name - and not only by the version requirement. Two entries for the same crate under different renames were previously indistinguishable whenever the version requirement ruled out both, so an edge could be kept because its sibling was activated. Neither narrowing is trusted to be exhaustive: one that would leave no candidate at all is skipped, so the check still fails open. Output is unchanged on this workspace and on four adversarial ones, under `--all`, `--top-level`, `--all-features`, `--no-default-features` and `--target all`. Also fixes the ordering of the `[CycloneDX#766]` link reference, and ignores the `Cargo.lock` that `cargo test` regenerates under `cyclonedx-bom-macros/tests/deps/`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Edvin Norling <edvin.norling@kognic.com>
Replaces the `BuiltEdges` set threaded through five functions with a single pass over the resolve map before any workspace member is processed. `index_dep_kinds`, `all_dependencies`, `top_level_dependencies`, `add_filtered_dependencies` and `filtered_dependencies` go back to their pre-change form, and pruning per `dep_kinds` entry keeps the `target` that a `(parent, child, kind)` key discarded. Also drops the narrowing by edge name in `is_built`: a resolve edge carries a single `PackageId`, so two renames of the same package cannot be told apart by it, and narrowing to the unactivated alias could drop an edge that is built. The `.gitignore` entry for the lockfile `cargo test` regenerates is unrelated to this fix and goes in its own PR. SBOM output is unchanged: identical components, versions, scopes and dependency edges on serde_with, reqwest, sqlx, rustls, polars and image. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Edvin Norling <edvin.norling@kognic.com>
nissessenap
force-pushed
the
fix/prune-unactivated-optional-deps
branch
from
August 27, 2026 07:45
f4bfdbd to
6f4597f
Compare
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.
Exclude optional dependencies that no enabled feature activates
Fixes #766.
The problem
cargo metadatalists optional dependencies inresolve.nodes[].depswhether or notthe resolver enabled them.
generator.rsnever read the feature information that comesback alongside them, so every optional dependency of every crate in the graph ended up
in the SBOM — along with everything reachable only through it.
Concretely, a crate whose only dependency is
serde_with 3gets an SBOM with 41components.
cargo tree -e normal,buildreports 13.This is also what makes the README's claim to "honor a particular combination of enabled
Cargo features" true rather than aspirational.
The fix
The resolve graph is pruned once, up front, by replaying the feature resolution
cargo metadataalready recorded and dropping the edges it shows were never taken:resolve.nodes[].featuresgives the features the resolver enabled on each package.packages[].featuresexpands those into the set of activated dependencies, followingdep:foo,foo/bar(activating),foo?/bar(weak, not activating), and plain featurenames recursively.
NodeDepis matched back to the[dependencies]entries that could have producedit — by package name, kind and target platform — and survives if any of them is
non-optional or activated. Renamed dependencies are keyed on the rename (
chrono_0_4),since that is what feature syntax refers to.
Because this happens before any workspace member is processed, the rest of the generator
is untouched:
filtered_dependencies(),all_dependencies()andindex_dep_kinds()seea resolve graph that no longer contains the edges, so components disappear along with them
and an edge that is never built cannot raise a component's
scope.Every lookup fails open — an edge whose manifest entry cannot be identified is kept, so
the change can remove false positives but not introduce false negatives.
Measured
Against
cargo tree -e normal,build --no-dedupeon the resolved graph:cargo treeserde_with 3reqwest 0.12(json,rustls-tls)sqlx 0.8(sqliteonly)rustls 0.23reqwest--target x86_64-pc-windows-msvcreqwest--target wasm32-unknown-unknowntokio 1(full),axum 0.8,clap 4(derive)polars 0.51(lazy)image 0.25Zero false negatives anywhere: nothing
cargo treereports is missing from the patchedoutput. No measurable runtime cost (0.28s either way on the 273-component
polarsgraph).What this does not fix
cargo metadataresolvesfeatures across the whole workspace, so a member inherits the union of its siblings'
features. The
polarsresidual above is this:cargo metadatagenuinely reportshexas enabled onpolars-ops, and an edge filter has to believe it.cfg(musl)/cfg(not(musl))featuresplits reports both branches in
node.features, with or without--filter-platform.The
imageresidual above is the related platform case (wasm-bindgenon a host build).Both are upstream in Cargo, and neither is made worse by this change.
Tests
dep:, implicit features, transitive expansion,foo/barvsfoo?/bar, renames, cyclic features, unknown features.tests/fixtures/optional_depsworkspace covering a non-optional dependency, oneactivated by
dep:, one activated only through its rename, one named only weakly, andone never named at all. It fails against
main.weak_depowns a private dependency, so pruning it cascades, and the same test assertsthe pruned graph leaves no dangling
dependenciesreferences.🤖 Generated with Claude Code