Skip to content

Commit c623a28

Browse files
findleyrgopherbot
authored andcommitted
gopls/internal/cache: fix crash in snapshot.Analyze with patch versions
Fix the same crash as golang/go#66195, this time in Analyze: don't set invalid Go versions on the types.Config. The largest part of this change was writing a realistic test, since the lack of a test for golang/go#66195 caused us to miss this additional location. It was possible to create a test that worked, by using a flag to select a go1.21 binary location. For this test, it was required to move a couple additional integration test preconditions into integration.Main (otherwise, the test would be skipped due to the modified environment). Fixes golang/go#66636 Change-Id: I24385474d4a6ebf6b7e9ae8f20948564bad3f55e Reviewed-on: https://go-review.googlesource.com/c/tools/+/576135 Auto-Submit: Robert Findley <[email protected]> Reviewed-by: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent f345449 commit c623a28

File tree

6 files changed

+116
-26
lines changed

6 files changed

+116
-26
lines changed

gopls/internal/cache/analysis.go

+1-4
Original file line numberDiff line numberDiff line change
@@ -1013,10 +1013,7 @@ func (an *analysisNode) typeCheck(parsed []*parsego.File) *analysisPackage {
10131013
// Set Go dialect.
10141014
if mp.Module != nil && mp.Module.GoVersion != "" {
10151015
goVersion := "go" + mp.Module.GoVersion
1016-
// types.NewChecker panics if GoVersion is invalid.
1017-
// An unparsable mod file should probably stop us
1018-
// before we get here, but double check just in case.
1019-
if goVersionRx.MatchString(goVersion) {
1016+
if validGoVersion(goVersion) {
10201017
cfg.GoVersion = goVersion
10211018
}
10221019
}

gopls/internal/cache/check.go

+21-13
Original file line numberDiff line numberDiff line change
@@ -1632,19 +1632,7 @@ func (b *typeCheckBatch) typesConfig(ctx context.Context, inputs typeCheckInputs
16321632

16331633
if inputs.goVersion != "" {
16341634
goVersion := "go" + inputs.goVersion
1635-
1636-
// types.NewChecker panics if GoVersion is invalid. An unparsable mod
1637-
// file should probably stop us before we get here, but double check
1638-
// just in case.
1639-
//
1640-
// Prior to go/[email protected] the precondition was stricter:
1641-
// no patch version. That's not a problem when also using go1.20 list,
1642-
// as it would reject go.mod files containing a patch version, but
1643-
// go/[email protected] will panic on go.mod versions that are returned
1644-
// by go1.21 list, hence the need for the extra check.
1645-
if goVersionRx.MatchString(goVersion) &&
1646-
(slices.Contains(build.Default.ReleaseTags, "go1.21") ||
1647-
strings.Count(goVersion, ".") < 2) { // no patch version
1635+
if validGoVersion(goVersion) {
16481636
cfg.GoVersion = goVersion
16491637
}
16501638
}
@@ -1655,6 +1643,26 @@ func (b *typeCheckBatch) typesConfig(ctx context.Context, inputs typeCheckInputs
16551643
return cfg
16561644
}
16571645

1646+
// validGoVersion reports whether goVersion is a valid Go version for go/types.
1647+
// types.NewChecker panics if GoVersion is invalid.
1648+
//
1649+
// Note that, prior to go1.21, go/types required exactly two components to the
1650+
// version number. For example, go types would panic with the Go version
1651+
// go1.21.1. validGoVersion handles this case when built with go1.20 or earlier.
1652+
func validGoVersion(goVersion string) bool {
1653+
if !goVersionRx.MatchString(goVersion) {
1654+
return false // malformed version string
1655+
}
1656+
1657+
// TODO(rfindley): remove once we no longer support building gopls with Go
1658+
// 1.20 or earlier.
1659+
if !slices.Contains(build.Default.ReleaseTags, "go1.21") && strings.Count(goVersion, ".") >= 2 {
1660+
return false // unsupported patch version
1661+
}
1662+
1663+
return true
1664+
}
1665+
16581666
// depsErrors creates diagnostics for each metadata error (e.g. import cycle).
16591667
// These may be attached to import declarations in the transitive source files
16601668
// of pkg, or to 'requires' declarations in the package's go.mod file.

gopls/internal/test/integration/regtest.go

+14
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,12 @@ func DefaultModes() Mode {
106106
return modes
107107
}
108108

109+
var runFromMain = false // true if Main has been called
110+
109111
// Main sets up and tears down the shared integration test state.
110112
func Main(m *testing.M, hook func(*settings.Options)) {
113+
runFromMain = true
114+
111115
// golang/go#54461: enable additional debugging around hanging Go commands.
112116
gocommand.DebugHangingGoCommands = true
113117

@@ -127,6 +131,16 @@ func Main(m *testing.M, hook func(*settings.Options)) {
127131
// Disable GOPACKAGESDRIVER, as it can cause spurious test failures.
128132
os.Setenv("GOPACKAGESDRIVER", "off")
129133

134+
if skipReason := checkBuilder(); skipReason != "" {
135+
fmt.Printf("Skipping all tests: %s\n", skipReason)
136+
os.Exit(0)
137+
}
138+
139+
if err := testenv.HasTool("go"); err != nil {
140+
fmt.Println("Missing go command")
141+
os.Exit(1)
142+
}
143+
130144
flag.Parse()
131145

132146
runner = &Runner{

gopls/internal/test/integration/runner.go

+13-7
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,14 @@ type TestFunc func(t *testing.T, env *Env)
135135
func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOption) {
136136
// TODO(rfindley): this function has gotten overly complicated, and warrants
137137
// refactoring.
138-
t.Helper()
139-
checkBuilder(t)
140-
testenv.NeedsGoPackages(t)
138+
139+
if !runFromMain {
140+
// Main performs various setup precondition checks.
141+
// While it could theoretically be made OK for a Runner to be used outside
142+
// of Main, it is simpler to enforce that we only use the Runner from
143+
// integration test suites.
144+
t.Fatal("integration.Runner.Run must be run from integration.Main")
145+
}
141146

142147
tests := []struct {
143148
name string
@@ -278,16 +283,17 @@ var longBuilders = map[string]string{
278283
"windows-arm-zx2c4": "",
279284
}
280285

281-
func checkBuilder(t *testing.T) {
282-
t.Helper()
286+
// TODO(rfindley): inline into Main.
287+
func checkBuilder() string {
283288
builder := os.Getenv("GO_BUILDER_NAME")
284289
if reason, ok := longBuilders[builder]; ok && testing.Short() {
285290
if reason != "" {
286-
t.Skipf("Skipping %s with -short due to %s", builder, reason)
291+
return fmt.Sprintf("skipping %s with -short due to %s", builder, reason)
287292
} else {
288-
t.Skipf("Skipping %s with -short", builder)
293+
return fmt.Sprintf("skipping %s with -short", builder)
289294
}
290295
}
296+
return ""
291297
}
292298

293299
type loggingFramer struct {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package workspace
6+
7+
import (
8+
"flag"
9+
"os"
10+
"runtime"
11+
"testing"
12+
13+
. "golang.org/x/tools/gopls/internal/test/integration"
14+
)
15+
16+
var go121bin = flag.String("go121bin", "", "bin directory containing go 1.21 or later")
17+
18+
// TODO(golang/go#65917): delete this test once we no longer support building
19+
// gopls with older Go versions.
20+
func TestCanHandlePatchVersions(t *testing.T) {
21+
// This test verifies the fixes for golang/go#66195 and golang/go#66636 --
22+
// that gopls does not crash when encountering a go version with a patch
23+
// number in the go.mod file.
24+
//
25+
// This is tricky to test, because the regression requires that gopls is
26+
// built with an older go version, and then the environment is upgraded to
27+
// have a more recent go. To set up this scenario, the test requires a path
28+
// to a bin directory containing go1.21 or later.
29+
if *go121bin == "" {
30+
t.Skip("-go121bin directory is not set")
31+
}
32+
33+
if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
34+
t.Skip("requires linux or darwin") // for PATH separator
35+
}
36+
37+
path := os.Getenv("PATH")
38+
t.Setenv("PATH", *go121bin+":"+path)
39+
40+
const files = `
41+
-- go.mod --
42+
module example.com/bar
43+
44+
go 1.21.1
45+
46+
-- p.go --
47+
package bar
48+
49+
type I interface { string }
50+
`
51+
52+
WithOptions(
53+
EnvVars{
54+
"PATH": path,
55+
},
56+
).Run(t, files, func(t *testing.T, env *Env) {
57+
env.OpenFile("p.go")
58+
env.AfterChange(
59+
NoDiagnostics(ForFile("p.go")),
60+
)
61+
})
62+
}

internal/testenv/testenv.go

+5-2
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ var checkGoBuild struct {
4545
err error
4646
}
4747

48-
func hasTool(tool string) error {
48+
// HasTool reports an error if the required tool is not available in PATH.
49+
//
50+
// For certain tools, it checks that the tool executable is correct.
51+
func HasTool(tool string) error {
4952
if tool == "cgo" {
5053
enabled, err := cgoEnabled(false)
5154
if err != nil {
@@ -198,7 +201,7 @@ func allowMissingTool(tool string) bool {
198201
// NeedsTool skips t if the named tool is not present in the path.
199202
// As a special case, "cgo" means "go" is present and can compile cgo programs.
200203
func NeedsTool(t testing.TB, tool string) {
201-
err := hasTool(tool)
204+
err := HasTool(tool)
202205
if err == nil {
203206
return
204207
}

0 commit comments

Comments
 (0)