Skip to content

Commit 5b4d426

Browse files
committed
gopls/internal/hooks: clean language version before passing to gofumpt
Fixes golang/go#61692 Change-Id: I97bd85c063ac1f525fd01c2b1a8b5ffe574e1c66 Reviewed-on: https://go-review.googlesource.com/c/tools/+/514815 TryBot-Result: Gopher Robot <[email protected]> gopls-CI: kokoro <[email protected]> Reviewed-by: Peter Weinberger <[email protected]> Run-TryBot: Robert Findley <[email protected]>
1 parent 2160c5f commit 5b4d426

File tree

3 files changed

+135
-1
lines changed

3 files changed

+135
-1
lines changed

gopls/internal/hooks/gofumpt_118.go

+55-1
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,70 @@ package hooks
99

1010
import (
1111
"context"
12+
"fmt"
1213

1314
"golang.org/x/tools/gopls/internal/lsp/source"
1415
"mvdan.cc/gofumpt/format"
1516
)
1617

1718
func updateGofumpt(options *source.Options) {
1819
options.GofumptFormat = func(ctx context.Context, langVersion, modulePath string, src []byte) ([]byte, error) {
20+
fixedVersion, err := fixLangVersion(langVersion)
21+
if err != nil {
22+
return nil, err
23+
}
1924
return format.Source(src, format.Options{
20-
LangVersion: langVersion,
25+
LangVersion: fixedVersion,
2126
ModulePath: modulePath,
2227
})
2328
}
2429
}
30+
31+
// fixLangVersion function cleans the input so that gofumpt doesn't panic. It is
32+
// rather permissive, and accepts version strings that aren't technically valid
33+
// in a go.mod file.
34+
//
35+
// More specifically, it looks for an optional 'v' followed by 1-3
36+
// '.'-separated numbers. The resulting string is stripped of any suffix beyond
37+
// this expected version number pattern.
38+
//
39+
// See also golang/go#61692: gofumpt does not accept the new language versions
40+
// appearing in go.mod files (e.g. go1.21rc3).
41+
func fixLangVersion(input string) (string, error) {
42+
bad := func() (string, error) {
43+
return "", fmt.Errorf("invalid language version syntax %q", input)
44+
}
45+
if input == "" {
46+
return input, nil
47+
}
48+
i := 0
49+
if input[0] == 'v' { // be flexible about 'v'
50+
i++
51+
}
52+
// takeDigits consumes ascii numerals 0-9 and reports if at least one was
53+
// consumed.
54+
takeDigits := func() bool {
55+
found := false
56+
for ; i < len(input) && '0' <= input[i] && input[i] <= '9'; i++ {
57+
found = true
58+
}
59+
return found
60+
}
61+
if !takeDigits() { // versions must start with at least one number
62+
return bad()
63+
}
64+
65+
// Accept optional minor and patch versions.
66+
for n := 0; n < 2; n++ {
67+
if i < len(input) && input[i] == '.' {
68+
// Look for minor/patch version.
69+
i++
70+
if !takeDigits() {
71+
i--
72+
break
73+
}
74+
}
75+
}
76+
// Accept any suffix.
77+
return input[:i], nil
78+
}
+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright 2022 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+
//go:build go1.18
6+
// +build go1.18
7+
8+
package hooks
9+
10+
import "testing"
11+
12+
func TestFixLangVersion(t *testing.T) {
13+
tests := []struct {
14+
input, want string
15+
wantErr bool
16+
}{
17+
{"", "", false},
18+
{"1.18", "1.18", false},
19+
{"v1.18", "v1.18", false},
20+
{"1.21", "1.21", false},
21+
{"1.21rc3", "1.21", false},
22+
{"1.21.0", "1.21.0", false},
23+
{"1.21.1", "1.21.1", false},
24+
{"v1.21.1", "v1.21.1", false},
25+
{"v1.21.0rc1", "v1.21.0", false}, // not technically valid, but we're flexible
26+
{"v1.21.0.0", "v1.21.0", false}, // also technically invalid
27+
{"1.1", "1.1", false},
28+
{"v1", "v1", false},
29+
{"1", "1", false},
30+
{"v1.21.", "v1.21", false}, // also invalid
31+
{"1.21.", "1.21", false},
32+
33+
// Error cases.
34+
{"rc1", "", true},
35+
{"x1.2.3", "", true},
36+
}
37+
38+
for _, test := range tests {
39+
got, err := fixLangVersion(test.input)
40+
if test.wantErr {
41+
if err == nil {
42+
t.Errorf("fixLangVersion(%q) succeeded unexpectedly", test.input)
43+
}
44+
continue
45+
}
46+
if err != nil {
47+
t.Fatalf("fixLangVersion(%q) failed: %v", test.input, err)
48+
}
49+
if got != test.want {
50+
t.Errorf("fixLangVersion(%q) = %s, want %s", test.input, got, test.want)
51+
}
52+
}
53+
}

gopls/internal/regtest/misc/formatting_test.go

+27
Original file line numberDiff line numberDiff line change
@@ -366,3 +366,30 @@ const Bar = 42
366366
}
367367
})
368368
}
369+
370+
func TestGofumpt_Issue61692(t *testing.T) {
371+
testenv.NeedsGo1Point(t, 21)
372+
373+
const input = `
374+
-- go.mod --
375+
module foo
376+
377+
go 1.21rc3
378+
-- foo.go --
379+
package foo
380+
381+
func _() {
382+
foo :=
383+
"bar"
384+
}
385+
`
386+
387+
WithOptions(
388+
Settings{
389+
"gofumpt": true,
390+
},
391+
).Run(t, input, func(t *testing.T, env *Env) {
392+
env.OpenFile("foo.go")
393+
env.FormatBuffer("foo.go") // golang/go#61692: must not panic
394+
})
395+
}

0 commit comments

Comments
 (0)