Skip to content
Closed
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## Unreleased

## 0.29.4 - 2026-10-20

- Compile governed Cargo replacements through their actual consumer, including
when only replacement source is selected. Retain source discovery and checks
without requiring dependency-owned locks or compiling dependency development
targets. Keep mixed Rust/Go execution aligned with the gate's command plan.

## 0.29.3 - 2026-10-18

- Resolve cyclic Rust glob exports and root `extern crate` aliases without
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.29.3
0.29.4
14 changes: 12 additions & 2 deletions docs/design/cargo-facts.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,8 +290,18 @@ invitation for the check to resolve dependencies.

Checks select the manifest's ordinary Cargo packages, default features and all
enabled targets, using the governed native host explicitly. They do not enable
every feature or establish coverage of other target configurations. Compiler
warnings, Clippy's default lint group and unused-code lints use `--force-warn` at
every feature or establish coverage of other target configurations.

Compiler selection maps explicitly governed replacement packages to their consumers,
using the same ownership boundary as locked resolution. Discovery retains the
replacement's own metadata for source analysis and also discovers its consumer.
Selecting only replacement source still checks the consumer; selecting both
does not compile it twice. Replacement development targets and their independent
locks are not part of the consumer build. Source analysis remains selected for
the replacement, while compiler evidence covers only dependencies active in the
consumer's default feature and host configuration.

Compiler warnings, Clippy's default lint group and unused-code lints use `--force-warn` at
the invocation boundary. Source `allow`, `expect`, `deny` and `forbid` attributes
cannot hide these diagnostics or turn policy lint collection into a compiler
failure. Code Polishy treats the resulting typed findings as blocking errors,
Expand Down
26 changes: 26 additions & 0 deletions internal/quality/cargo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,32 @@ func TestCargoAndGoCheckPlanMatchesObservedCommandOrder(t *testing.T) {
}
}

func TestCargoReplacementCheckKeepsGoCommandsInsideThePlan(t *testing.T) {
t.Parallel()
repo := cargoQualityRepository(t)
repo.Config.SupplyChain.DependencyOverridePolicies = []policy.DependencyOverridePolicy{{Ecosystem: "cargo", Path: "Cargo.toml", Field: "patch.crates-io"}}
writeQualityFile(t, repo.Root, "Cargo.toml", "[package]\nname='app'\nversion='1.0.0'\nedition='2024'\n[workspace]\nexclude=['vendor/library']\n[dependencies]\nlibrary='=1.0.0'\n[patch.crates-io]\nlibrary={path='vendor/library'}\n")
writeQualityFile(t, repo.Root, "Cargo.lock", "version=4\n[[package]]\nname='app'\nversion='1.0.0'\ndependencies=['library']\n[[package]]\nname='library'\nversion='1.0.0'\n")
writeQualityFile(t, repo.Root, "src/lib.rs", "pub fn app() {\n library::call();\n}\n")
writeQualityFile(t, repo.Root, "vendor/library/Cargo.toml", "[package]\nname='library'\nversion='1.0.0'\nedition='2024'\n")
writeQualityFile(t, repo.Root, "vendor/library/src/lib.rs", "pub fn call() {}\n")
writeQualityFile(t, repo.Root, "go.mod", "module example.test/mixed\n\ngo 1.26.6\n")
writeQualityFile(t, repo.Root, "main.go", "package main\n\nfunc main() {}\n")
selection := repository.Selection{Files: []string{"main.go", "src/lib.rs", "vendor/library/src/lib.rs"}}
planned := CheckCommands(repo, selection, "check")
boundary := &cargoRecordingRunner{OSRunner: runner.OSRunner{}}
if findings := Check(t.Context(), repo, selection, boundary, "check"); len(findings) != 0 {
t.Fatalf("replacement prevented the mixed-language check: %+v", findings)
}
want := []string{}
for _, command := range planned {
want = append(want, command.Name)
}
if !reflect.DeepEqual(boundary.names, want) {
t.Fatalf("executed commands = %v, planned = %v", boundary.names, want)
}
}

func TestCargoNativeDeadCodePreservesPublicAPIAndChecksSelectedFeatures(t *testing.T) {
t.Parallel()
repo := cargoQualityRepository(t)
Expand Down
4 changes: 3 additions & 1 deletion internal/rustfacts/compilation.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ func PlanCompilation(repo repository.Repository, discovery DiscoveryPlan, allFil
if len(discovery.Commands) == 0 {
return plan
}
for _, manifest := range discovery.Manifests {
manifests, _, findings := cargoResolutionRoots(repo, discovery.Manifests)
plan.Findings = append(plan.Findings, findings...)
for _, manifest := range manifests {
files := compilationSources(repo, manifest, allFiles)
native, err := repo.NativeUnit(files, "lint", "typecheck", "dead-code")
if err != nil {
Expand Down
49 changes: 49 additions & 0 deletions internal/rustfacts/compilation_patches_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package rustfacts

import (
"bytes"
"os"
"path/filepath"
"slices"
"testing"

"github.com/riteofstring/code-polishy/internal/policy"
"github.com/riteofstring/code-polishy/internal/runner"
)

func TestRustCompilationUsesReplacementConsumerAndPreservesDependencyTargets(t *testing.T) {
t.Parallel()
for _, selected := range []string{"Cargo.toml", "vendor/patch-source/src/lib.rs", "vendor/leaf/src/lib.rs"} {
t.Run(selected, func(t *testing.T) {
repo := cargoPatchRepository(t)
manifest := string(readRustFile(t, repo, "Cargo.toml"))
manifest = string(bytes.ReplaceAll([]byte(manifest), []byte("default=[]"), []byte("default=['desktop']")))
write(t, repo.Root, "Cargo.toml", manifest)
write(t, repo.Root, "src/lib.rs", "pub fn value() -> u32 { patch_source::value() }\n")
write(t, repo.Root, "vendor/patch-source/src/lib.rs", "pub fn value() -> u32 { patch_leaf::value() }\n")
write(t, repo.Root, "vendor/patch-source/build.rs", "fn main() {}\n")
write(t, repo.Root, "vendor/patch-source/tests/unconsumed.rs", "compile_error!(\"dependency development targets must not compile\");\n")
write(t, repo.Root, "vendor/leaf/src/lib.rs", "pub fn value() -> u32 { 42 }\n")
before := readRustFile(t, repo, "Cargo.lock")
plan, discovery := compilationDiscovery(t, repo, selected)
if findings := Compile(t.Context(), repo, plan, discovery, runner.OSRunner{}); len(findings) != 0 {
t.Fatalf("consumer compilation failed: %+v", findings)
}
if !bytes.Equal(before, readRustFile(t, repo, "Cargo.lock")) {
t.Fatal("compilation changed the consumer lock")
}
for _, path := range []string{"vendor/patch-source/Cargo.lock", "vendor/leaf/Cargo.lock"} {
if _, err := os.Stat(filepath.Join(repo.Root, filepath.FromSlash(path))); !os.IsNotExist(err) {
t.Fatalf("compilation created a replacement lock at %s: %v", path, err)
}
}
write(t, repo.Root, "vendor/leaf/src/lib.rs", "pub fn value() -> u32 { \"wrong\" }\n")
findings := Compile(t.Context(), repo, plan, discovery, runner.OSRunner{})
if !slices.ContainsFunc(findings, func(f policy.Finding) bool {
return f.Check == "quality.rustCompiler" && f.Subject == "E0308" && f.Path == "vendor/leaf/src/lib.rs"
}) {
t.Fatalf("consumer failed to check the replacement source: %+v", findings)
}
})
}
}
5 changes: 5 additions & 0 deletions internal/rustfacts/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ func PlanDiscovery(repo repository.Repository, selected, allFiles []string) Disc
if len(manifests) == 0 {
return plan
}
consumers, _, findings := cargoResolutionRoots(repo, manifests)
plan.Findings = append(plan.Findings, findings...)
manifests = append(manifests, consumers...)
slices.Sort(manifests)
manifests = slices.Compact(manifests)
if err := configurationProblem(repo); err != nil {
plan.Findings = append(plan.Findings, projectFinding(manifests[0], "configuration", err.Error()))
return plan
Expand Down
Loading