Repository navigation
Fix Go replace target missed in go 1.16 graphs (#1840) - #1843
Mikola Lysenko (mikolalysenko) wants to merge 5 commits into
Conversation
Assisted-by: Claude Code:claude-opus-5-5
In a go 1.16 module, when a dependency imports a fork that the root go.mod replaces with the patched upstream module, apply reported the patch applied, apply --check said in sync and vex attested not_affected, while go build linked the unpatched code. The root go.mod of an unpruned graph does not list modules that only a dependency requires, but its replace still applies to them. The replace-target check only trusted a fork listed in the root require, so it missed this shape. Now, in an unpruned graph, such a replace counts as in effect unless go.mod requires the patched module itself. Fixes #1840 Assisted-by: Claude Code:claude-opus-5-5
The first fix guessed that a fork replace in a go 1.16 module is in effect whenever go.mod doesn't list the fork. That refused working patches when go mod tidy kept a replace nothing uses, and the hosted rewriter then removed a working Socket replace. It also missed a fork a dependency raises to the replaced version. Now, when an unpruned go.mod has a module-to-module replace, the module graph is read from the module cache, the same walk go.work projects use, and the fork's selected version decides. apply, apply --check, the hosted rewriter and VEX discovery all use it. When the cache lacks go.mod files, the safe answer stays: the patch is not reported built. Refs #1840 Assisted-by: Claude Code:claude-opus-5-5
Reading the go 1.16 module graph went around the project view, which ended read recording for every project: VEX discovery asked for the raw disk root even with no Go files, so discovery could no longer be reused between passes. Now the graph is read only when go.mod needs it, and the root go.mod and any directory replacement's go.mod are declared as reads, so a change to them still invalidates the cache. Refs #1840 Assisted-by: Claude Code:claude-opus-5-5
Without every go.mod of a go 1.16 module graph in the module cache, a fork replace is only suspected of bypassing the Socket pin. That guess now refuses a new pin but no longer removes an existing one that may be working. A partial graph is no longer trusted as proof that the fork is at a lower version. Socket's own hosted replaces no longer trigger a graph walk. Refs #1840 Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 4e9eebf. Configure here.
|
Burn-down agent: labeled Ready for review at
Generated by Claude Code |
Final review briefWhat it does. In a go 1.16 (unpruned) module, a root Risk: medium. It changes the selection rule that Look here
Verified
Changes I made: none. Open questions: none blocking. The PR body lists known non-goals: Auto-merge (squash) is armed, so approving sends it straight to the merge queue. Generated by Claude Code |
|
[agent] Heads-up: #1893 (fix for #1862) is stacked on this branch; it reuses the Generated by Claude Code |
LLM Description written by Claude Code:claude-opus-5-5
Fixes #1840
What was wrong
Take a go 1.16 module (or one with no
godirective) where a dependency importsother.com/fork, and the root go.mod hasreplace other.com/fork => example.com/upstream v1.0.0. A patch forexample.com/upstream v1.0.0was reported as applied.apply --checksaid it was in sync, andvexattestednot_affected. Meanwhilego buildlinked the unpatched code, because go doesn't chain replaces.Root cause
replaced_module(crates/socket-patch-core/src/vendor/go_build_list.rs) decides whether a module-to-module replace explains the upstream's go.sum zip line. It only counted the replace when the root go.mod'srequirelisted the fork. In an unpruned graph (go < 1.17), the root go.mod leaves out modules that only a dependency requires. The root'sreplacestill applies to them. Itsrequireversions are also only lower bounds. So go.mod alone can't tell whether the fork is in the build, or at which version.Fix
ModuleGraphreads it from GOMODCACHE. It reusesGoWorkspace::read_graph_from, the walker go.work projects already use (as a one-member workspace). The fork's MVS-selected version then decides whether the replace is in effect. That covers a fork only a dependency requires, a fork a dependency raises past go.mod's require, a versioned replace shadowing a version-less one, and a stale replace thatgo mod tidykept but nothing uses.select_in/selection_intake the graph. All callers use it:project_selection(agentapply,apply --check, CLIvex), the hosted rewriter (throughHostedGoWorkspace.module_graph, read by the hosted engine), and VEX lockfile discovery. The graph is only read when the go.mod needs it. The files it reads in the project (root go.mod, directory replacements' go.mod) are declared to the snapshot view, so cached discovery stays valid.module@versionitself: go refuses one module@version under two paths. That guess only refuses a new hosted pin. It never removes an existing one (proves_replace_target). Only a pruned go.mod or a complete graph removes a stale pin.Tests (red → green)
e2e_golang_build::fork_required_only_by_a_dependency_is_a_replace_targetapply --checkexit 0 "Patches are in sync" (build prints PRISTINE)go_build_list::tests::unpruned_replace_of_a_transitive_fork_is_a_replace_targete2e_golang_build::stale_fork_replace_keeps_a_transitive_patchgo_build_list::tests::unpruned_module_graph_settles_the_fork_replace,unpruned_root_require_of_another_version_keeps_the_fork_replace,only_a_pruned_go_mod_or_a_complete_graph_proves_a_replace_target,module_graph_is_needed_only_for_an_unpruned_fork_replacego_workspace::tests::module_graph_reaches_a_fork_only_a_dependency_requiresredirect::tests::golang_go116_module_graph_decides_the_fork_replace,golang_go116_suspected_replace_target_keeps_the_hosted_pinOne existing assertion changed meaning. In
inactive_replace_target_keeps_the_zip_as_evidence, the go 1.16 cases "fork not required" and "version-less replace shadowed at go.mod's required version" encoded the bug: go.mod alone can't show the fork is absent, or that a dependency didn't raise it. They now run only for go 1.21. The go 1.16 behavior is covered with and without a graph inunpruned_module_graph_settles_the_fork_replace.Commands run locally (Linux, go 1.24.7):
cargo fmt --all -- --check: cleancargo clippy --workspace --all-features -- -D warnings: cleancargo test -p socket-patch-core --all-features: 6999 passed, 4 failed. The 4 failures are chmod-based write-failure tests (copy_tree,vlt_heal,pypi_poetry,pypi_requirements) that can't fail as root (this container runs as uid 0). They're unrelated to Go, and none of their modules is touched by this PR.cargo test -p socket-patch-cli --all-features --libplus every Go-touching suite withSOCKET_PATCH_GO_E2E_REQUIRED=1:e2e_golang_build(33),e2e_golang_hosted_build(25),e2e_golang_workspace_build(31),e2e_vendor_golang_build(44),e2e_golang,e2e_golang_hosted_state,e2e_vex,e2e_vex_redirect,e2e_vex_vendor,ecosystem_dispatch_e2e,in_process_*,cli_apply_silent: all pass.cargo test --workspace(189 CLI test binaries) doesn't fit this container's disk, so CI covers the rest./code-review high findings not fixed
excludedirectives aren't applied by the graph walker. That's the same pre-existing limitation as go.work mode. An excluded fork version can only make a patch refused, never falsely attested.project_selection: the graph is read only for a module that an unpruned go.mod's fork replace targets (needs_module_graph), not for every patch.let in_effect = …; in_effectinreplaced_moduleis required: the iterator borrowsentries, a local, so it can't be the tail expression (E0597). Clippy is clean.Out of scope:
GoWorkspace::selectionnever returnsReplaceTarget, so the #1823/#1840 shapes inside a go.work workspace aren't detected. That's pre-existing and not part of #1840.https://claude.ai/code/session_011f9aeNyyce3TLFE4vQrpsJ
Generated by Claude Code