fix(cargo): resolve inherited workspace dependency identity

(cherry picked from commit 4d8fa2e93c)
This commit is contained in:
oleksii-tumanov authored and safishamsi committed 2026-09-22 15:00:07 +01:00
1 parent 9b42507014
commit 7a508a71bd
2 files changed
+133

No files matched your search

+17
View File
@@ -86,6 +86,10 @@ def introspect_cargo(root: str | Path) -> dict[str, Any]:
if discovered is not None:
root_manifest = discovered
root_data = _load_toml(root_manifest)
workspace = root_data.get("workspace")
workspace_dependencies = workspace.get("dependencies", {}) if isinstance(workspace, dict) else {}
if not isinstance(workspace_dependencies, dict):
workspace_dependencies = {}
# Workspace members are resolved relative to the manifest's OWN
# directory, not necessarily root_path (#3678: the manifest can live in
@@ -119,6 +123,13 @@ def introspect_cargo(root: str | Path) -> dict[str, Any]:
continue
source_file = manifest.relative_to(root_path).as_posix()
for dep_key, spec in sorted(dependencies.items()):
inherited = isinstance(spec, dict) and "workspace" in spec
if inherited:
if spec["workspace"] is not True:
continue
spec = workspace_dependencies.get(dep_key)
if not isinstance(spec, dict) or not isinstance(spec.get("path"), str):
continue
# Cargo lets a dep table entry rename the crate via `package = "..."`:
# db = { path = "../storage", package = "internal-storage" }
# The key `db` is the name used in `use db::…;`; the actual crate
@@ -133,6 +144,12 @@ def introspect_cargo(root: str | Path) -> dict[str, Any]:
target = crates.get(real_name)
if target is None:
continue
if inherited:
# Shared paths are relative to the workspace root, not the member.
# A matching name alone cannot prove a registry/git dep is local.
inherited_manifest = (root_path / spec["path"] / "Cargo.toml").resolve()
if inherited_manifest != target[1].resolve():
continue
edges.append(
{
"source": source_id,
+116
View File
@@ -565,3 +565,119 @@ edition = "2021"
with pytest.raises(FileNotFoundError):
introspect_cargo(tmp_path)
@pytest.mark.parametrize("root_package", [False, True], ids=["virtual", "root-package"])
def test_cargo_inherited_rename_uses_workspace_dependency_identity(tmp_path, root_package):
root_header = '[package]\nname = "root-app"\nversion = "0.1.0"\n' if root_package else ""
root_dependency = '[dependencies]\ndb.workspace = true\n' if root_package else ""
_write_manifest(
tmp_path / "Cargo.toml",
root_header
+ '[workspace]\nmembers = ["app", "storage"]\n'
+ '[workspace.dependencies]\n'
+ 'db = { path = "storage", package = "internal-storage" }\n'
+ root_dependency,
)
app = tmp_path / "app"
storage = tmp_path / "storage"
app.mkdir()
storage.mkdir()
_write_manifest(
app / "Cargo.toml",
'[package]\nname = "app"\nversion = "0.1.0"\n'
'[dependencies]\ndb = { workspace = true, features = ["fast"] }\n',
)
_write_manifest(
storage / "Cargo.toml",
'[package]\nname = "internal-storage"\nversion = "0.1.0"\n'
'[features]\nfast = []\n',
)
result = introspect_cargo(tmp_path)
expected_sources = {"crate:app"}
if root_package:
expected_sources.add("crate:root-app")
assert {node["id"] for node in result["nodes"]} == expected_sources | {"crate:internal-storage"}
assert len(result["edges"]) == len(expected_sources)
assert {(edge["source"], edge["target"]) for edge in result["edges"]} == {
(source, "crate:internal-storage") for source in expected_sources
}
for edge in result["edges"]:
assert edge["source_file"] == ("Cargo.toml" if edge["source"] == "crate:root-app" else "app/Cargo.toml")
assert edge["relation"] == "crate_depends_on"
assert edge["context"] == "cargo_dependency"
assert edge["confidence"] == "EXTRACTED"
assert introspect_cargo(tmp_path) == result
@pytest.mark.parametrize(
("root_declaration", "member_declaration", "expected"),
[
('db = { path = "db" }', 'db.workspace = true', True),
('db = "1"', 'db.workspace = true', False),
('db = { version = "1" }', 'db.workspace = true', False),
('db = { git = "https://example.com/db" }', 'db.workspace = true', False),
('db = { path = "missing" }', 'db.workspace = true', False),
('db = { path = "app" }', 'db.workspace = true', False),
('db = { path = 42 }', 'db.workspace = true', False),
('db = false', 'db.workspace = true', False),
('', 'db.workspace = true', False),
('db = { path = "db" }', 'db.workspace = false', False),
('db = { path = "db" }', 'db.workspace = "true"', False),
('db = { path = "missing" }', 'db = { path = "../db" }', True),
],
)
def test_cargo_inherited_dependency_requires_matching_local_path(
tmp_path, root_declaration, member_declaration, expected
):
_write_manifest(
tmp_path / "Cargo.toml",
'[workspace]\nmembers = ["app", "db"]\n'
'[workspace.dependencies]\n' + root_declaration + '\n',
)
for name in ("app", "db"):
crate = tmp_path / name
crate.mkdir()
_write_manifest(
crate / "Cargo.toml",
f'[package]\nname = "{name}"\nversion = "0.1.0"\n'
+ (f'[dependencies]\n{member_declaration}\n' if name == "app" else ""),
)
result = introspect_cargo(tmp_path)
assert {node["id"] for node in result["nodes"]} == {"crate:app", "crate:db"}
assert [(edge["source"], edge["target"]) for edge in result["edges"]] == (
[("crate:app", "crate:db")] if expected else []
)
@pytest.mark.parametrize("root_path", [".", ""])
def test_cargo_inherited_dependency_can_target_root_package(tmp_path, root_path):
_write_manifest(
tmp_path / "Cargo.toml",
'[package]\nname = "db"\nversion = "0.1.0"\n'
'[workspace]\nmembers = ["app"]\n'
f'[workspace.dependencies]\ndb = {{ path = "{root_path}" }}\n',
)
app = tmp_path / "app"
app.mkdir()
_write_manifest(
app / "Cargo.toml",
'[package]\nname = "app"\nversion = "0.1.0"\n'
'[dependencies]\ndb.workspace = true\n',
)
result = introspect_cargo(tmp_path)
assert [(edge["source"], edge["target"]) for edge in result["edges"]] == [
("crate:app", "crate:db")
]
@pytest.mark.parametrize("workspace", ["", "workspace = false\n", "[workspace]\n", "[workspace]\ndependencies = false\n"])
def test_cargo_missing_or_malformed_workspace_dependencies_are_ignored(tmp_path, workspace):
_write_manifest(
tmp_path / "Cargo.toml",
workspace + '[package]\nname = "db"\nversion = "0.1.0"\n'
'[dependencies]\ndb.workspace = true\n',
)
assert introspect_cargo(tmp_path)["edges"] == []