Skip to content

x/tools/gopls: stale shouldLoad map causes spurious go.mod modifications after branch switch #79585

Description

@zainanjianglin

x/tools/gopls: stale shouldLoad map causes spurious go.mod modifications after git branch switch

Environment

go version go1.25.5 darwin/arm64
gopls v0.21.1
VSCode 1.96+ with Go extension v0.52.2

Steps to reproduce

  1. In VSCode, open a Go project on branch A. Code depends on example.com/org/some-module. gopls loads workspace normally.
  2. Use git (terminal or VSCode source control) to switch to branch B. Branch B does NOT depend on example.com/org/some-module (not in go.mod, no import in code).
  3. Observe go.mod is automatically modified by gopls, adding example.com/org/some-module back as // indirect.

Expected behavior

go.mod remains unchanged after switching to branch B.

Actual behavior

gopls repeatedly adds the old branch's dependency back to go.mod. go mod tidy removes it, but gopls adds it back again. In VSCode this manifests as go.mod showing perpetual unsaved changes that the user did not make.

Verification

go mod tidy                                    # removes example.com/org/some-module
go list -mod=mod -test ./...                   # example.com/org/some-module added back (reproduces gopls behavior)
go list -mod=readonly -test ./...              # no modification
go mod why example.com/org/some-module         # "main module does not need package ..."

Root cause

In gopls/internal/cache/snapshot.go, snapshot cloning preserves shouldLoad unconditionally:

shouldLoad: s.shouldLoad.Clone(), // not cloneWithout: shouldLoad is cleared on loads

Branch switch triggers reinit=true, but shouldLoad (which records package IDs needing reload) is fully cloned from the old snapshot without being cleared. reloadWorkspace() then iterates these stale package IDs and calls packages.Load() (without -mod=readonly). The go command resolves these stale packages from the local module cache (downloaded while on branch A) and writes them into go.mod.

Suggested fix

Clear shouldLoad when reinit=true, or validate entries against the current module graph before attempting reload.


中文说明

复现场景

在 VSCode 中打开一个 Go 项目,当前处于分支 A,代码依赖了 example.com/org/some-module,gopls 正常加载了整个工作区。随后通过终端或 VSCode 的源代码管理面板执行 git checkout 切换到分支 B,分支 B 的 go.mod 中没有该模块,代码中也没有任何 import 引用它。

切换完成后,VSCode 中的 go.mod 文件出现了未保存的修改——gopls 自动把 example.com/org/some-module// indirect 的形式重新加回了 go.mod。在 VSCode 的文件标签上会看到一个圆点标记,表示文件被改动了,但这不是用户自己的操作。

手动执行 go mod tidy 可以删掉这个多余的依赖(因为确实没有代码引用),但过一会儿 gopls 又会把它加回来,形成反复循环。

验证方式

go mod tidy                                    # 成功删掉该模块
go list -mod=mod -test ./...                   # 该模块又被加回来(复现了 gopls 的行为)
go list -mod=readonly -test ./...              # go.mod 不会被修改
go mod why example.com/org/some-module         # 输出 "main module does not need package ..."

以上验证说明:该模块确实不被代码需要,问题出在 go list 未使用 -mod=readonly 时的 module graph 解析行为。而 gopls 内部调用 packages.Load() 时恰好没有设置 -mod=readonly

根因分析

定位到 gopls/internal/cache/snapshot.go 中 snapshot 克隆逻辑:

shouldLoad: s.shouldLoad.Clone(), // not cloneWithout: shouldLoad is cleared on loads

shouldLoad 是一个持久化的 map,记录了"哪些 package ID 需要被重新加载"。切换分支时 go.mod 在磁盘上发生变化,gopls 检测到后会触发 reinit=true 并创建新的 snapshot。但问题是:新 snapshot 的 shouldLoad 是从旧 snapshot 完整克隆过来的,并没有因为 reinit=true 而被清空。

这导致后续的处理流程出现问题:

  1. reloadWorkspace() 遍历 shouldLoad,其中仍然包含旧分支上那些依赖 example.com/org/some-module 的 package ID
  2. 对这些 package ID 调用 s.load()packages.Load()(底层是 go list,未设置 -mod=readonly
  3. go list 在解析这些旧 package 的模块依赖时,从本地 module cache 中找到了 example.com/org/some-module(这是在旧分支工作时下载到本地的)
  4. 由于运行在 -mod=mod 模式下,go list 直接将该模块写入 go.mod
  5. go mod tidy 通过实际 import 链路分析发现该模块无用,将其删除
  6. 删除又触发 gopls 检测到 go.mod 变化,重复上述过程

建议修复方向

reinit=true(通常意味着 go.mod/go.work 在磁盘上发生了变化,即切换分支或手动编辑)时,应该清空 shouldLoad 而不是从旧 snapshot 克隆。或者在 reload 前校验 shouldLoad 中的每个 package ID 是否仍然属于当前 go.mod 的 module graph,过滤掉已经不存在的条目。

Metadata

Metadata

Labels

BugReportIssues describing a possible bug in the Go implementation.ToolsThis label describes issues relating to any tools in the x/tools repository.goplsIssues related to the Go language server, gopls.

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions