Skip to content

Commit 1202974

Browse files
findleyrgopherbot
authored andcommitted
[gopls-release-branch.0.15] 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). Updates golang/go#66636 Updates golang/go#66730 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]> (cherry picked from commit c623a28) Reviewed-on: https://go-review.googlesource.com/c/tools/+/577301
1 parent 3051f4f commit 1202974

File tree

6 files changed

+118
-28
lines changed

6 files changed

+118
-28
lines changed

gopls/internal/cache/analysis.go

+3-6
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ import (
3232

3333
"golang.org/x/sync/errgroup"
3434
"golang.org/x/tools/go/analysis"
35+
"golang.org/x/tools/gopls/internal/cache/metadata"
3536
"golang.org/x/tools/gopls/internal/file"
3637
"golang.org/x/tools/gopls/internal/filecache"
37-
"golang.org/x/tools/gopls/internal/cache/metadata"
38-
"golang.org/x/tools/gopls/internal/protocol"
3938
"golang.org/x/tools/gopls/internal/progress"
39+
"golang.org/x/tools/gopls/internal/protocol"
4040
"golang.org/x/tools/gopls/internal/settings"
4141
"golang.org/x/tools/gopls/internal/util/astutil"
4242
"golang.org/x/tools/gopls/internal/util/bug"
@@ -1012,10 +1012,7 @@ func (an *analysisNode) typeCheck(parsed []*ParsedGoFile) *analysisPackage {
10121012
// Set Go dialect.
10131013
if mp.Module != nil && mp.Module.GoVersion != "" {
10141014
goVersion := "go" + mp.Module.GoVersion
1015-
// types.NewChecker panics if GoVersion is invalid.
1016-
// An unparsable mod file should probably stop us
1017-
// before we get here, but double check just in case.
1018-
if goVersionRx.MatchString(goVersion) {
1015+
if validGoVersion(goVersion) {
10191016
typesinternal.SetGoVersion(cfg, goVersion)
10201017
}
10211018
}

gopls/internal/cache/check.go

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

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

1644+
// validGoVersion reports whether goVersion is a valid Go version for go/types.
1645+
// types.NewChecker panics if GoVersion is invalid.
1646+
//
1647+
// Note that, prior to go1.21, go/types required exactly two components to the
1648+
// version number. For example, go types would panic with the Go version
1649+
// go1.21.1. validGoVersion handles this case when built with go1.20 or earlier.
1650+
func validGoVersion(goVersion string) bool {
1651+
if !goVersionRx.MatchString(goVersion) {
1652+
return false // malformed version string
1653+
}
1654+
1655+
// TODO(rfindley): remove once we no longer support building gopls with Go
1656+
// 1.20 or earlier.
1657+
if !slices.Contains(build.Default.ReleaseTags, "go1.21") && strings.Count(goVersion, ".") >= 2 {
1658+
return false // unsupported patch version
1659+
}
1660+
1661+
return true
1662+
}
1663+
16561664
// depsErrors creates diagnostics for each metadata error (e.g. import cycle).
16571665
// These may be attached to import declarations in the transitive source files
16581666
// 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
@@ -271,16 +276,17 @@ var longBuilders = map[string]string{
271276
"windows-arm-zx2c4": "",
272277
}
273278

274-
func checkBuilder(t *testing.T) {
275-
t.Helper()
279+
// TODO(rfindley): inline into Main.
280+
func checkBuilder() string {
276281
builder := os.Getenv("GO_BUILDER_NAME")
277282
if reason, ok := longBuilders[builder]; ok && testing.Short() {
278283
if reason != "" {
279-
t.Skipf("Skipping %s with -short due to %s", builder, reason)
284+
return fmt.Sprintf("skipping %s with -short due to %s", builder, reason)
280285
} else {
281-
t.Skipf("Skipping %s with -short", builder)
286+
return fmt.Sprintf("skipping %s with -short", builder)
282287
}
283288
}
289+
return ""
284290
}
285291

286292
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 {
@@ -192,7 +195,7 @@ func allowMissingTool(tool string) bool {
192195
// NeedsTool skips t if the named tool is not present in the path.
193196
// As a special case, "cgo" means "go" is present and can compile cgo programs.
194197
func NeedsTool(t testing.TB, tool string) {
195-
err := hasTool(tool)
198+
err := HasTool(tool)
196199
if err == nil {
197200
return
198201
}

0 commit comments

Comments
 (0)