Skip to content

Cargo: Fix transitive dev-dependency classification#1692

Open
spatten wants to merge 16 commits intomasterfrom
cargo-dev-labelling-fix
Open

Cargo: Fix transitive dev-dependency classification#1692
spatten wants to merge 16 commits intomasterfrom
cargo-dev-labelling-fix

Conversation

@spatten
Copy link
Copy Markdown
Contributor

@spatten spatten commented Apr 13, 2026

Overview

The cargo strategy was labeling transitive dependencies by the kind of the single edge directly pointing at them. This caused deps reachable only via a dev-dep subtree to be reported as Production whenever the intermediate edges happened to be normal-kind — for example workspace → approx (dev) → num-traits (normal) leaked num-traits into Production.

The fix in src/Strategy/Cargo.hs replaces per-edge labeling with two edge-kind-aware reachability passes over the resolved graph:

  • Production: reachable from workspace members through paths of null-kind edges only. A "build" or "dev" edge breaks the release chain, so its target (and everything transitively below) is not Production via that path.
  • Development: reachable (via any edge) from the target of any non-null-kind edge. A build or dev edge marks the root of a subtree that is never linked into the release binary, and every descendant of that subtree inherits Development.

A package reachable by both kinds of paths carries both labels — LabeledGrapher already supports multi-labels, so no structural changes were needed.

Why we don't need a separate rule for transitive dev-deps

Cargo only resolves [dev-dependencies] for workspace members. Dev-deps declared inside a non-workspace crate are not part of cargo metadata's resolve graph, so the only non-null kind we ever see on a non-workspace edge is "build". The reachability rule above handles it uniformly.

Acceptance criteria

  • Transitive deps reached only via dev-dep or build-dep roots are labeled Development (previously leaked to Production when intermediate edges were normal-kind).
  • Build-dependencies of production deps (e.g. autocfg, cc, version_check) and their transitives are labeled Development — they're needed to compile the project but are never linked into the release artifact.
  • A dep reachable from both a Production root and a Development root gets both labels (multi-label preserved).
  • Projects with non-trivial dev-dep or build-dep trees will see their Production set shrink and their Development set grow. This is a correctness fix but is a visible behavior change, called out in the changelog.

Testing plan

Minimal reproduction

The Cargo.toml includes two dependencies. itoa has no transitive deps. It is included as a prod dependency, so it should be the only production dependency in the graph.

approx is being included as a dev dependency. It has two transitive dependencies: approx requires num-traits as a normal dependency, and num-traits requires autocfg as a build-script dependency.

Before this fix, fossa analyze reported both itoa and num-traits as production dependencies. After the fix, it correctly reports only itoa as a production dependency.

  1. Create /tmp/cargo-devdep-test/Cargo.toml:

    [package]
    name = "cargo-devdep-test"
    version = "0.1.0"
    edition = "2021"
    
    [dependencies]
    itoa = "1"
    
    [dev-dependencies]
    approx = "0.5"
  2. Create /tmp/cargo-devdep-test/src/lib.rs:

    pub fn add(a: u64, b: u64) -> u64 { a + b }
  3. Generate the lockfile:

    cd /tmp/cargo-devdep-test && cargo generate-lockfile
  4. Run fossa (released) and fossa-dev (this branch):

    cd /tmp/cargo-devdep-test
    fossa analyze --output | jq
    make install-dev
    fossa-dev analyze --output | jq

Compare the two outputs. The output from fossa-dev only includes cargo+itoa. The output from fossa includes both cargo+itoa and cargo+num-traits.

Run again with --include-unused-deps. All four deps appear in the graph; the difference is that approx, num-traits, and autocfg are now labeled Development instead of leaking into Production.

Real-world verification: sparkle and ficus

This is where the build-dep correctness is visible — the minimal num-traits-style repro above doesn't differentiate the fix from released fossa (both classify autocfg as Development, just by different paths). The sparkle/ficus scans are the evidence that the fix handles the build-dep case at scale without regressing any Production deps.

I scanned both projects with released fossa (3.16.6) and fossa-dev from this branch, then diffed sourceUnits.Build.Dependencies:

Project Released This PR Delta
ficus 370 355 −15
sparkle 724 693 −31

All movement is Production → Development — no deps incorrectly leaked the other direction. The reclassified deps are exactly the categories you'd expect:

  • Test frameworks: proptest, wiremock, tokio-test, axum-test, simple_test_case, test-log, tracing-test-macro, assert-json-diff
  • Build-script utilities: autocfg, cc, cfg_aliases, pkg-config, rustc_version, vcpkg, version_check, jobserver, shlex, find-msvc-tools
  • Dev utilities: xshell, xshell-macros, cargo_metadata, cargo-platform, env_logger, env_filter, tempfile
  • Platform-specific deps of dev tooling: winapi-i686-pc-windows-gnu, winapi-x86_64-pc-windows-gnu, hermit-abi

Automated coverage

New MetadataSpec cases:

  • Transitive deps of dev-deps labeled Development (the original bug).
  • Dep reachable from both prod and dev roots gets both labels.
  • Direct dep declared as both normal and dev gets both labels.
  • Build-deps of prod-deps and their transitives are Development (catches the regression that a naïve propagation approach would reintroduce).
  • Dep reachable via a normal prod path and a build-edge gets both labels (multi-label under the edge-kind-aware algorithm).

Risks

Correctness fix with a visible behavior change: Production dep sets will shrink for any project that has dev-dep or build-dep trees with non-trivial transitive closures. This was the intent — previously those trees were partially leaking into Production. Flagged in the changelog. No deps move in the opposite direction (Dev → Prod), so there's no risk of over-reporting.

Metrics

Not tracked.

References

  • Plan: cargo-dev-prod-labeling-fix.md

Checklist

  • I added tests for this PR's change (or explained in the PR description why tests don't make sense).
  • If this PR introduced a user-visible change, I added documentation into docs/.
  • If this PR added docs, I added links as appropriate to the user manual's ToC in docs/README.ms and gave consideration to how discoverable or not my documentation is.
  • If this change is externally visible, I updated Changelog.md. If this PR did not mark a release, I added my changes into an ## Unreleased section at the top.
  • If I made changes to .fossa.yml or fossa-deps.{json.yml}, I updated docs/references/files/*.schema.json AND I have updated example files used by fossa init command. You may also need to update these if you have added/removed new dependency type (e.g. pip) or analysis target type (e.g. poetry).
  • If I made changes to a subcommand's options, I updated docs/references/subcommands/<subcommand>.md.

spatten added a commit that referenced this pull request Apr 13, 2026
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@spatten spatten force-pushed the cargo-dev-labelling-fix branch 3 times, most recently from bf1a46b to e999111 Compare April 15, 2026 22:48
spatten and others added 13 commits April 15, 2026 15:53
…ssification

Instead of labeling each transitive dependency based on the kind of
the single edge pointing at it, propagate labels from workspace roots:
BFS from production roots (normal-kind direct deps) labels reachable
packages as Production, BFS from development roots (dev/build-kind
direct deps) labels them as Development. This fixes transitive deps
in dev-dep subtrees being mislabeled as Production when intermediate
edges happen to be normal-kind.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add two test cases:
1. Transitive deps of dev-deps (approx->num-traits->autocfg) are all
   labeled Development, while prod deps (itoa) remain Production.
2. A dep reachable from both prod and dev roots gets both labels.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the explicit reachability BFS with workspace-member edge
labeling plus hydrateDepEnvs propagation. Same correctness fix as
before — transitive deps reached only via dev-dep roots are labeled
Development, multi-label preserved — but relies on the existing
graph hydration helper instead of bespoke adjacency/BFS code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Promote workspaceMembers from [PackageId] to Set PackageId so the
per-node membership check is O(log n) instead of O(n). Also expand
the kindToLabel comment to document the "build"/"dev"/null mapping.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous hydrateDepEnvs-based approach propagated Production
downward through any edge, which mislabeled build-script utilities
(autocfg, cc, version_check, etc.) as Production whenever they
appeared as [build-dependencies] of a normal dep. Verified on sparkle
(+16 false Production deps pre-fix) and ficus (+5 pre-fix).

Replace with two explicit reachability passes:

  * Production: reachable from workspace members via null-kind edges
    only. A build or dev edge breaks the release chain.
  * Development: anything reachable (via any edge) from the target of
    a non-null-kind edge — the build/dev subtree and its transitives.

Cargo does not resolve transitive dev-dependencies (only workspace-
member dev-deps), so the only non-null kind on non-workspace edges is
"build". Comment captures this so future readers know why we don't
need a separate propagation rule for dev.

Adds two regression tests covering build-dep subtrees and the multi-
label case where a dep is reachable via both a prod-normal path and
a prod→build edge.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per CLAUDE.md style guide, avoid list comprehensions. Refactor the
reachability adjacency builders to use map/filter/concatMap, and
factor the edge-kind predicates into isProdEdge/isDevEdge helpers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Other strategies (pnpm, yarn) propagate EnvProduction/EnvDevelopment via
the shared hydrateDepEnvs helper. Cargo cannot, because hydrateDepEnvs
has no edge-kind awareness and would incorrectly tag packages reached
only through "dev"- or "build"-kind edges as Production. Call this out
explicitly so future readers don't "simplify" back to hydration and
reintroduce the bug this PR fixes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@spatten spatten force-pushed the cargo-dev-labelling-fix branch from 8b058a0 to 9f6664f Compare April 15, 2026 22:54
Rebase onto master picked up the 3.17.2 release section. Consolidate
the Cargo entry there (3.17.2 is not yet released) and remove the
duplicate 3.17.1 heading.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@spatten spatten force-pushed the cargo-dev-labelling-fix branch from 9f6664f to 6bf3943 Compare April 15, 2026 22:54
Per hlint suggestion — single traversal instead of two.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@spatten spatten marked this pull request as ready for review April 16, 2026 00:16
@spatten spatten requested a review from a team as a code owner April 16, 2026 00:16
@spatten spatten requested review from GauravB159 and csasarak April 16, 2026 00:16
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 16, 2026

Walkthrough

This pull request refactors the Cargo strategy to correctly classify dependencies reachable only via dev-dependencies or build-dependencies as "Development" rather than "Production". The implementation replaces per-edge labeling with a graph reachability analysis approach. It introduces adjacency maps to distinguish production-only paths from paths including development dependencies, computes reachable node sets for each classification, and labels packages based on their membership in these sets. A package can receive both labels when reachable via both path types. Test coverage is added for transitive dev-dependency labeling scenarios and packages with dual reachability paths.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main fix: correcting how transitive dev-dependencies are classified in the Cargo strategy.
Description check ✅ Passed The description comprehensively covers all required template sections: overview, acceptance criteria, testing plan with concrete steps, risks, and references. All checklist items are marked complete.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Strategy/Cargo.hs`:
- Around line 560-568: The function reachable uses guarded equations in the
helper go (the clause "go visited (x : xs) | Set.member x visited = ..." and the
otherwise branch); replace that guard-based pattern with a single if-then-else
expression inside the "go visited (x : xs)" equation: compute children as
before, then use if Set.member x visited then go visited xs else go (Set.insert
x visited) (children ++ xs). Keep the same names (reachable, go, visited,
children) and preserve semantics and types.

In `@test/Cargo/MetadataSpec.hs`:
- Around line 209-340: Add a new test in MetadataSpec (near
devDepTransitiveLabelingSpec) that constructs a multi-member workspace using
CargoMetadata with two workspace members (e.g., member-a and member-b) where
member-a has a NodeDependency on member-b and member-b depends on an external
crate; call buildGraph False meta and assert that the external crate appears in
expectDeps/expectDirect but the internal workspace member (member-b) does not
surface as an external dependency. Use the existing helpers/constructors from
the file (PackageId, mkPkgId, ResolveNode, NodeDependency, devKind/nullKind,
mkDep, expectDeps, expectDirect) so the test covers the regression of labeling
before shrinkRoots.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

Run ID: 86ddddd6-6146-422b-9f41-2800c9ccba59

📥 Commits

Reviewing files that changed from the base of the PR and between d855f0e and db4fe4e.

📒 Files selected for processing (3)
  • Changelog.md
  • src/Strategy/Cargo.hs
  • test/Cargo/MetadataSpec.hs

Comment thread src/Strategy/Cargo.hs
Comment on lines +560 to +568
reachable :: Map.Map PackageId [PackageId] -> Set PackageId -> Set PackageId
reachable adj = go Set.empty . Set.toList
where
go visited [] = visited
go visited (x : xs)
| Set.member x visited = go visited xs
| otherwise =
let children = fromMaybe [] (Map.lookup x adj)
in go (Set.insert x visited) (children ++ xs)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and examine the specific lines
if [ -f "src/Strategy/Cargo.hs" ]; then
  echo "=== File exists. Checking lines 560-568 ==="
  sed -n '560,568p' src/Strategy/Cargo.hs
else
  echo "File not found at src/Strategy/Cargo.hs"
  # Try to find it
  find . -name "Cargo.hs" -type f 2>/dev/null | head -5
fi

Repository: fossas/fossa-cli

Length of output: 461


🏁 Script executed:

# Get more context around lines 560-568
sed -n '555,575p' src/Strategy/Cargo.hs

Repository: fossas/fossa-cli

Length of output: 765


🏁 Script executed:

# Check the overall structure of the file to understand imports and pattern
head -100 src/Strategy/Cargo.hs | tail -50

Repository: fossas/fossa-cli

Length of output: 1368


🏁 Script executed:

# Search for any Haskell coding guidelines in the repo
find . -type f \( -name "*.md" -o -name "*.txt" -o -name "GUIDELINES*" -o -name ".editorconfig" \) | xargs grep -l "match guard\|pattern guard\|avoid.*guard" 2>/dev/null | head -10

Repository: fossas/fossa-cli

Length of output: 106


🏁 Script executed:

# Check the coding guidelines files
cat ./CLAUDE.md

Repository: fossas/fossa-cli

Length of output: 1497


🏁 Script executed:

# Also check the style guide
cat ./docs/contributing/STYLE-GUIDE.md

Repository: fossas/fossa-cli

Length of output: 12249


🏁 Script executed:

# Verify the proposed fix would be syntactically correct by checking if similar patterns exist elsewhere
rg "if.*then.*else" src/Strategy/Cargo.hs | head -10

Repository: fossas/fossa-cli

Length of output: 42


Replace the guarded equations in reachable with an if-then-else expression.

This code uses match guards, which the repository explicitly avoids per the style guide. Replace with conditional expressions as shown below.

♻️ Proposed fix
 reachable :: Map.Map PackageId [PackageId] -> Set PackageId -> Set PackageId
 reachable adj = go Set.empty . Set.toList
   where
     go visited [] = visited
-    go visited (x : xs)
-      | Set.member x visited = go visited xs
-      | otherwise =
-          let children = fromMaybe [] (Map.lookup x adj)
-           in go (Set.insert x visited) (children ++ xs)
+    go visited (x : xs) =
+      if Set.member x visited
+        then go visited xs
+        else
+          let children = fromMaybe [] (Map.lookup x adj)
+           in go (Set.insert x visited) (children ++ xs)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
reachable :: Map.Map PackageId [PackageId] -> Set PackageId -> Set PackageId
reachable adj = go Set.empty . Set.toList
where
go visited [] = visited
go visited (x : xs)
| Set.member x visited = go visited xs
| otherwise =
let children = fromMaybe [] (Map.lookup x adj)
in go (Set.insert x visited) (children ++ xs)
reachable :: Map.Map PackageId [PackageId] -> Set PackageId -> Set PackageId
reachable adj = go Set.empty . Set.toList
where
go visited [] = visited
go visited (x : xs) =
if Set.member x visited
then go visited xs
else
let children = fromMaybe [] (Map.lookup x adj)
in go (Set.insert x visited) (children ++ xs)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Strategy/Cargo.hs` around lines 560 - 568, The function reachable uses
guarded equations in the helper go (the clause "go visited (x : xs) | Set.member
x visited = ..." and the otherwise branch); replace that guard-based pattern
with a single if-then-else expression inside the "go visited (x : xs)" equation:
compute children as before, then use if Set.member x visited then go visited xs
else go (Set.insert x visited) (children ++ xs). Keep the same names (reachable,
go, visited, children) and preserve semantics and types.

Comment on lines +209 to +340
devDepTransitiveLabelingSpec :: Test.Spec
devDepTransitiveLabelingSpec =
Test.describe "dev-dep transitive labeling" $ do
Test.it "transitive deps of dev-deps are labeled Development" $ do
let wsId = PackageId "myapp" "0.1.0" "path+file:///path/to/myapp"
itoaId = mkPkgId "itoa" "1.0.18"
approxId = mkPkgId "approx" "0.5.1"
numTraitsId = mkPkgId "num-traits" "0.2.19"
autocfgId = mkPkgId "autocfg" "1.4.0"

wsNode =
ResolveNode
wsId
[ NodeDependency itoaId [nullKind]
, NodeDependency approxId [devKind "dev"]
]
approxNode =
ResolveNode
approxId
[ NodeDependency numTraitsId [nullKind]
]
numTraitsNode =
ResolveNode
numTraitsId
[ NodeDependency autocfgId [devKind "build"]
]
itoaNode = ResolveNode itoaId []
autocfgNode = ResolveNode autocfgId []

meta = CargoMetadata [] [wsId] $ Resolve [wsNode, approxNode, numTraitsNode, itoaNode, autocfgNode]
graph = buildGraph False meta

itoaDep = mkDep "itoa" "1.0.18" CargoType [EnvProduction]
approxDep = mkDep "approx" "0.5.1" CargoType [EnvDevelopment]
numTraitsDep = mkDep "num-traits" "0.2.19" CargoType [EnvDevelopment]
autocfgDep = mkDep "autocfg" "1.4.0" CargoType [EnvDevelopment]

expectDeps [itoaDep, approxDep, numTraitsDep, autocfgDep] graph
expectDirect [itoaDep, approxDep] graph

Test.it "dep reachable from both prod and dev roots gets both labels" $ do
let wsId = PackageId "myapp" "0.1.0" "path+file:///path/to/myapp"
aId = mkPkgId "A" "1.0.0"
bId = mkPkgId "B" "1.0.0"
cId = mkPkgId "C" "1.0.0"

wsNode =
ResolveNode
wsId
[ NodeDependency aId [nullKind]
, NodeDependency bId [devKind "dev"]
]
aNode = ResolveNode aId [NodeDependency cId [nullKind]]
bNode = ResolveNode bId [NodeDependency cId [nullKind]]
cNode = ResolveNode cId []

meta = CargoMetadata [] [wsId] $ Resolve [wsNode, aNode, bNode, cNode]
graph = buildGraph False meta

aDep = mkDep "A" "1.0.0" CargoType [EnvProduction]
bDep = mkDep "B" "1.0.0" CargoType [EnvDevelopment]
cDep = mkDep "C" "1.0.0" CargoType [EnvProduction, EnvDevelopment]

expectDeps [aDep, bDep, cDep] graph
expectDirect [aDep, bDep] graph

Test.it "direct dep declared as both normal and dev gets both labels" $ do
-- Cargo emits multiple NodeDepKinds when a dep is declared in both
-- [dependencies] and [dev-dependencies] on the same package.
let wsId = PackageId "myapp" "0.1.0" "path+file:///path/to/myapp"
dualId = mkPkgId "dual" "1.0.0"
transId = mkPkgId "trans" "1.0.0"

wsNode = ResolveNode wsId [NodeDependency dualId [nullKind, devKind "dev"]]
dualNode = ResolveNode dualId [NodeDependency transId [nullKind]]
transNode = ResolveNode transId []

meta = CargoMetadata [] [wsId] $ Resolve [wsNode, dualNode, transNode]
graph = buildGraph False meta

dualDep = mkDep "dual" "1.0.0" CargoType [EnvProduction, EnvDevelopment]
transDep = mkDep "trans" "1.0.0" CargoType [EnvProduction, EnvDevelopment]

expectDeps [dualDep, transDep] graph
expectDirect [dualDep] graph

Test.it "build-deps of prod-deps and their transitives are Development" $ do
-- Regression guard: build-script utilities (autocfg, cc, version_check,
-- etc.) are declared as [build-dependencies] of normal deps. They are
-- compiled during the build but never linked into the release artifact,
-- so they should be labeled Development, not Production — even though
-- their parent is a production dep.
let wsId = PackageId "myapp" "0.1.0" "path+file:///path/to/myapp"
serdeId = mkPkgId "serde" "1.0.0"
autocfgId = mkPkgId "autocfg" "1.0.0"
autocfgChildId = mkPkgId "autocfg-child" "1.0.0"

wsNode = ResolveNode wsId [NodeDependency serdeId [nullKind]]
serdeNode = ResolveNode serdeId [NodeDependency autocfgId [devKind "build"]]
autocfgNode = ResolveNode autocfgId [NodeDependency autocfgChildId [nullKind]]
childNode = ResolveNode autocfgChildId []

meta = CargoMetadata [] [wsId] $ Resolve [wsNode, serdeNode, autocfgNode, childNode]
graph = buildGraph False meta

serdeDep = mkDep "serde" "1.0.0" CargoType [EnvProduction]
autocfgDep = mkDep "autocfg" "1.0.0" CargoType [EnvDevelopment]
childDep = mkDep "autocfg-child" "1.0.0" CargoType [EnvDevelopment]

expectDeps [serdeDep, autocfgDep, childDep] graph
expectDirect [serdeDep] graph

Test.it "dep reachable via a normal prod path and a build-edge gets both labels" $ do
-- X is a direct normal dep of the workspace (Production) AND a build-dep
-- of a prod dep (Development via the build-edge split). Both labels
-- should be applied.
let wsId = PackageId "myapp" "0.1.0" "path+file:///path/to/myapp"
aId = mkPkgId "A" "1.0.0"
xId = mkPkgId "X" "1.0.0"

wsNode = ResolveNode wsId [NodeDependency aId [nullKind], NodeDependency xId [nullKind]]
aNode = ResolveNode aId [NodeDependency xId [devKind "build"]]
xNode = ResolveNode xId []

meta = CargoMetadata [] [wsId] $ Resolve [wsNode, aNode, xNode]
graph = buildGraph False meta

aDep = mkDep "A" "1.0.0" CargoType [EnvProduction]
xDep = mkDep "X" "1.0.0" CargoType [EnvProduction, EnvDevelopment]

expectDeps [aDep, xDep] graph
expectDirect [aDep, xDep] graph
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add a multi-member workspace regression case.

These scenarios all use a single workspace root. Since buildGraph now labels workspace members before shrinkRoots, please add a member-a -> member-b -> dep case to ensure internal workspace crates still do not surface as dependencies.

🧪 Suggested coverage
     Test.it "dep reachable via a normal prod path and a build-edge gets both labels" $ do
       -- X is a direct normal dep of the workspace (Production) AND a build-dep
       -- of a prod dep (Development via the build-edge split). Both labels
       -- should be applied.
@@
       expectDeps [aDep, xDep] graph
       expectDirect [aDep, xDep] graph
+
+    Test.it "workspace members do not surface as dependencies" $ do
+      let appId = PackageId "app" "0.1.0" "path+file:///path/to/app"
+          libId = PackageId "lib" "0.1.0" "path+file:///path/to/lib"
+          depId = mkPkgId "dep" "1.0.0"
+
+          appNode = ResolveNode appId [NodeDependency libId [nullKind]]
+          libNode = ResolveNode libId [NodeDependency depId [nullKind]]
+          depNode = ResolveNode depId []
+
+          meta = CargoMetadata [] [appId, libId] $ Resolve [appNode, libNode, depNode]
+          graph = buildGraph False meta
+
+          dep = mkDep "dep" "1.0.0" CargoType [EnvProduction]
+          libDep = mkDep "/path/to/lib" "0.1.0" UnresolvedPathType [EnvProduction]
+
+      expectDeps [dep] graph
+      expectDirect [dep] graph
+      Graphing.vertexList graph `shouldSatisfy` notElem libDep

As per coding guidelines, **/*.{hs,rs}: Code should be thoroughly tested with appropriate unit tests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/Cargo/MetadataSpec.hs` around lines 209 - 340, Add a new test in
MetadataSpec (near devDepTransitiveLabelingSpec) that constructs a multi-member
workspace using CargoMetadata with two workspace members (e.g., member-a and
member-b) where member-a has a NodeDependency on member-b and member-b depends
on an external crate; call buildGraph False meta and assert that the external
crate appears in expectDeps/expectDirect but the internal workspace member
(member-b) does not surface as an external dependency. Use the existing
helpers/constructors from the file (PackageId, mkPkgId, ResolveNode,
NodeDependency, devKind/nullKind, mkDep, expectDeps, expectDirect) so the test
covers the regression of labeling before shrinkRoots.

Style guide avoids match guards.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@GauravB159 GauravB159 removed their request for review April 21, 2026 20:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant