Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions desloppify/languages/rust/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
parse_cargo_errors,
parse_clippy_messages,
run_rustdoc_result,
scope_cargo_command,
)

DetectCommand = Callable[[argparse.Namespace], None]
Expand Down Expand Up @@ -180,11 +181,15 @@ def command(args: argparse.Namespace) -> None:

cmd_clippy_warning = _make_tool_detect_command(
RUST_CLIPPY_LABEL,
lambda path: run_tool_result(RUST_CLIPPY_CMD, path, parse_clippy_messages),
lambda path: run_tool_result(
scope_cargo_command(RUST_CLIPPY_CMD, path), path, parse_clippy_messages
),
)
cmd_cargo_error = _make_tool_detect_command(
RUST_CHECK_LABEL,
lambda path: run_tool_result(RUST_CHECK_CMD, path, parse_cargo_errors),
lambda path: run_tool_result(
scope_cargo_command(RUST_CHECK_CMD, path), path, parse_cargo_errors
),
)
cmd_rustdoc_warning = _make_tool_detect_command(
RUST_RUSTDOC_LABEL,
Expand Down
9 changes: 7 additions & 2 deletions desloppify/languages/rust/phases.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
parse_cargo_errors,
parse_clippy_messages,
run_rustdoc_result,
scope_cargo_command,
)

RUST_CLIPPY_LABEL = "cargo clippy"
Expand Down Expand Up @@ -266,7 +267,9 @@ def run(path: Path, lang) -> tuple[list[dict], dict[str, int]]:
def tool_phase_clippy():
return _make_rust_tool_phase(
RUST_CLIPPY_LABEL,
lambda path: run_tool_result(RUST_CLIPPY_CMD, path, parse_clippy_messages),
lambda path: run_tool_result(
scope_cargo_command(RUST_CLIPPY_CMD, path), path, parse_clippy_messages
),
"clippy_warning",
tier=2,
)
Expand All @@ -275,7 +278,9 @@ def tool_phase_clippy():
def tool_phase_check():
return _make_rust_tool_phase(
RUST_CHECK_LABEL,
lambda path: run_tool_result(RUST_CHECK_CMD, path, parse_cargo_errors),
lambda path: run_tool_result(
scope_cargo_command(RUST_CHECK_CMD, path), path, parse_cargo_errors
),
"cargo_error",
tier=3,
)
Expand Down
4 changes: 2 additions & 2 deletions desloppify/languages/rust/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,7 @@ def _resolve_local_dependency_entry(


def find_workspace_root(path: Path | str | None) -> Path:
"""Return the outermost Cargo workspace root for a file/dir when present."""
"""Return the nearest Cargo workspace root without escaping nested worktrees."""
if path is None:
return get_project_root()

Expand All @@ -541,7 +541,7 @@ def find_workspace_root(path: Path | str | None) -> Path:
continue
workspace = data.get("workspace")
if isinstance(workspace, dict):
workspace_root = current
return current.resolve()
return workspace_root.resolve()


Expand Down
100 changes: 100 additions & 0 deletions desloppify/languages/rust/tests/test_scan_scope.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Regression coverage for nested worktrees and member-scoped Rust scans."""

import json
import shlex
import subprocess

import pytest

from desloppify.languages.rust.support import find_workspace_root
from desloppify.languages.rust.tools import (
CARGO_ERROR_CMD,
CLIPPY_WARNING_CMD,
run_rustdoc_result,
scope_cargo_command,
)


def workspace_with_members(root):
root.mkdir(parents=True)
(root / "Cargo.toml").write_text('[workspace]\nmembers = ["a", "b"]\n')
for name in ("a", "b"):
package = root / name
(package / "src").mkdir(parents=True)
(package / "Cargo.toml").write_text(
f'[package]\nname = "{name}"\nversion = "0.1.0"\n'
)
(package / "src" / "lib.rs").write_text("pub fn value() {}\n")
return root


def test_workspace_discovery_stops_at_nested_worktree(tmp_path):
outer = workspace_with_members(tmp_path / "outer")
worktree = workspace_with_members(outer / ".worktrees" / "cleanup")
(worktree / ".git").write_text("gitdir: ../../.git/worktrees/cleanup\n")

assert find_workspace_root(worktree / "a" / "src") == worktree
assert find_workspace_root(worktree) == worktree
assert find_workspace_root(outer / "a" / "src") == outer


@pytest.mark.parametrize("command", [CARGO_ERROR_CMD, CLIPPY_WARNING_CMD])
def test_member_commands_preserve_checks_without_selecting_siblings(tmp_path, command):
workspace = workspace_with_members(tmp_path / "workspace with spaces")
scoped = scope_cargo_command(command, workspace / "a" / "src")
args = shlex.split(scoped)

assert "--workspace" not in args
assert args[args.index("--manifest-path") + 1] == str(
workspace / "a" / "Cargo.toml"
)
assert "--all-targets" in args
assert "--all-features" in args
assert "--message-format=json" in args
assert scope_cargo_command(command, workspace) == command


@pytest.mark.parametrize("member_scan", [False, True])
def test_rustdoc_scopes_members_and_accepts_metadata_stderr(tmp_path, member_scan):
workspace = workspace_with_members(tmp_path / "workspace")
metadata = {
"workspace_members": ["a-id", "b-id"],
"packages": [
{
"id": f"{name}-id",
"name": name,
"manifest_path": str(workspace / name / "Cargo.toml"),
"targets": [{"kind": ["lib"], "crate_types": ["lib"]}],
}
for name in ("a", "b")
],
}
commands = []

def runner(args, **kwargs):
assert kwargs["cwd"] == str(workspace)
command = args[2] if args[:2] == ["/bin/sh", "-lc"] else " ".join(args)
commands.append(command)
if command.startswith("cargo metadata"):
return subprocess.CompletedProcess(
args, 0, json.dumps(metadata), "warning: optional workspace metadata\n"
)
return subprocess.CompletedProcess(args, 0, "", "")

scan_path = workspace / "a" / "src" if member_scan else workspace
result = run_rustdoc_result(scan_path, run_subprocess=runner)

assert result.status == "empty"
assert any("--package a " in command for command in commands)
assert any("--package b " in command for command in commands) is not member_scan


def test_rustdoc_metadata_failure_is_not_hidden(tmp_path):
workspace = workspace_with_members(tmp_path / "workspace")

def runner(args, **kwargs):
return subprocess.CompletedProcess(args, 1, "", "manifest could not be loaded")

result = run_rustdoc_result(workspace, run_subprocess=runner)
assert result.status == "error"
assert "manifest could not be loaded" in result.message
31 changes: 27 additions & 4 deletions desloppify/languages/rust/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
resolve_command_argv,
run_tool_result,
)
from desloppify.languages.rust.support import find_workspace_root
from desloppify.languages.rust.support import find_manifest_dir, find_workspace_root

CLIPPY_WARNING_CMD = (
"cargo clippy --workspace --all-targets --all-features --message-format=json "
Expand Down Expand Up @@ -594,6 +594,15 @@ def build_rustdoc_warning_cmd(package: str) -> str:
return RUSTDOC_WARNING_CMD.format(package=shlex.quote(package))


def scope_cargo_command(command: str, scan_path: Path) -> str:
"""Select the enclosing member for a member scan, not every workspace crate."""
manifest_dir = find_manifest_dir(scan_path)
if manifest_dir is None or manifest_dir.resolve() == find_workspace_root(scan_path):
return command
manifest = shlex.quote(str((manifest_dir / "Cargo.toml").resolve()))
return command.replace("--workspace", f"--manifest-path {manifest}", 1)


def _entry_file_exists(entry: dict[str, Any], workspace_root: Path) -> bool:
file_name = entry.get("file")
if not isinstance(file_name, str) or not file_name.strip():
Expand All @@ -612,12 +621,25 @@ def _filter_existing_rustdoc_entries(
return [entry for entry in entries if _entry_file_exists(entry, workspace_root)]


def _extract_workspace_rustdoc_packages(payload: dict[str, Any]) -> list[str]:
def _extract_workspace_rustdoc_packages(
payload: dict[str, Any], scan_path: Path | None = None
) -> list[str]:
workspace_members = set(payload.get("workspace_members") or [])
packages: list[str] = []
for package in payload.get("packages") or []:
if not isinstance(package, dict) or package.get("id") not in workspace_members:
continue
if scan_path is not None:
manifest_path = package.get("manifest_path")
if not isinstance(manifest_path, str):
continue
package_root = Path(manifest_path).resolve().parent
scan_root = scan_path.resolve()
if not (
package_root.is_relative_to(scan_root)
or scan_root.is_relative_to(package_root)
):
continue
name = package.get("name")
if not isinstance(name, str) or not name.strip():
continue
Expand Down Expand Up @@ -690,7 +712,7 @@ def _run_cargo_metadata(
[],
)
try:
data = json.loads(output)
data = json.loads(result.stdout or "")
except json.JSONDecodeError as exc:
return (
ToolRunResult(
Expand All @@ -713,7 +735,8 @@ def _run_cargo_metadata(
),
[],
)
return None, _extract_workspace_rustdoc_packages(data)
scoped_path = scan_path if scan_path.resolve() != workspace_root else None
return None, _extract_workspace_rustdoc_packages(data, scoped_path)


def run_rustdoc_result(
Expand Down