Skip to content

Commit 3cad859

Browse files
committed
gopls/internal/analysis/modernize: quick fixes to modernize Go code
This CL is the start of an analyzer that offers a suite of code transformations to modernize Go code, by taking advantage of newer features of the language when it would clarify the code to do so. Because it reports diagnostics on correct code, its severity is downgraded to INFO. Only one is implemented so far: the replacement of if/else conditional assignments by a call to min or max. + relnote and test Updates golang/go#70815 Change-Id: I686db57c2b95dfa399b44e5c6fdc478272cee084 Reviewed-on: https://go-review.googlesource.com/c/tools/+/635777 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Robert Findley <[email protected]>
1 parent 8194029 commit 3cad859

File tree

12 files changed

+448
-4
lines changed

12 files changed

+448
-4
lines changed

gopls/doc/analyzers.md

+14
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,20 @@ Default: on.
436436

437437
Package documentation: [lostcancel](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/lostcancel)
438438

439+
<a id='modernize'></a>
440+
## `modernize`: simplify code by using modern constructs
441+
442+
443+
This analyzer reports opportunities for simplifying and clarifying
444+
existing code by using more modern features of Go, such as:
445+
446+
- replacing if/else conditional assignments by a call to the
447+
built-in min or max functions.
448+
449+
Default: on.
450+
451+
Package documentation: [modernize](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize)
452+
439453
<a id='nilfunc'></a>
440454
## `nilfunc`: check for useless comparisons between functions and nil
441455

gopls/doc/release/v0.18.0.md

+12
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@
44

55
# New features
66

7+
## New `modernize` analyzer
8+
9+
Gopls will now report when code could be simplified or clarified by
10+
using more modern features of Go, and provides a quick fix to apply
11+
the change.
12+
13+
Examples:
14+
15+
- replacement of conditional assignment using an if/else statement by
16+
a call to the `min` or `max` built-in functions added in Go 1.18;
17+
18+
719
## "Implementations" supports generics
820

921
At long last, the "Go to Implementations" feature now fully supports
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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 modernize providers the modernizer analyzer.
6+
//
7+
// # Analyzer modernize
8+
//
9+
// modernize: simplify code by using modern constructs
10+
//
11+
// This analyzer reports opportunities for simplifying and clarifying
12+
// existing code by using more modern features of Go, such as:
13+
//
14+
// - replacing if/else conditional assignments by a call to the
15+
// built-in min or max functions.
16+
package modernize
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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+
//go:build ignore
6+
7+
// The modernize command suggests (or, with -fix, applies) fixes that
8+
// clarify Go code by using more modern features.
9+
package main
10+
11+
import (
12+
"golang.org/x/tools/go/analysis/singlechecker"
13+
"golang.org/x/tools/gopls/internal/analysis/modernize"
14+
)
15+
16+
func main() { singlechecker.Main(modernize.Analyzer) }
+190
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
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 modernize
6+
7+
import (
8+
"fmt"
9+
"go/ast"
10+
"go/token"
11+
"go/types"
12+
13+
"golang.org/x/tools/go/analysis"
14+
"golang.org/x/tools/go/analysis/passes/inspect"
15+
"golang.org/x/tools/go/ast/inspector"
16+
"golang.org/x/tools/internal/astutil/cursor"
17+
)
18+
19+
// The minmax pass replaces if/else statements with calls to min or max.
20+
//
21+
// Patterns:
22+
//
23+
// 1. if a < b { x = a } else { x = b } => x = min(a, b)
24+
// 2. x = a; if a < b { x = b } => x = max(a, b)
25+
//
26+
// Variants:
27+
// - all four ordered comparisons
28+
// - "x := a" or "x = a" or "var x = a" in pattern 2
29+
// - "x < b" or "a < b" in pattern 2
30+
func minmax(pass *analysis.Pass) {
31+
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
32+
33+
for curIfStmt := range cursor.Root(inspect).Preorder((*ast.IfStmt)(nil)) {
34+
ifStmt := curIfStmt.Node().(*ast.IfStmt)
35+
36+
if compare, ok := ifStmt.Cond.(*ast.BinaryExpr); ok &&
37+
isInequality(compare.Op) != 0 &&
38+
isAssignBlock(ifStmt.Body) {
39+
40+
tassign := ifStmt.Body.List[0].(*ast.AssignStmt)
41+
42+
// Have: if a < b { lhs = rhs }
43+
var (
44+
a = compare.X
45+
b = compare.Y
46+
lhs = tassign.Lhs[0]
47+
rhs = tassign.Rhs[0]
48+
)
49+
50+
scope := pass.TypesInfo.Scopes[ifStmt.Body]
51+
sign := isInequality(compare.Op)
52+
53+
if fblock, ok := ifStmt.Else.(*ast.BlockStmt); ok && isAssignBlock(fblock) {
54+
fassign := fblock.List[0].(*ast.AssignStmt)
55+
56+
// Have: if a < b { lhs = rhs } else { lhs2 = rhs2 }
57+
lhs2 := fassign.Lhs[0]
58+
rhs2 := fassign.Rhs[0]
59+
60+
// For pattern 1, check that:
61+
// - lhs = lhs2
62+
// - {rhs,rhs2} = {a,b}
63+
if equalSyntax(lhs, lhs2) {
64+
if equalSyntax(rhs, a) && equalSyntax(rhs2, b) {
65+
sign = +sign
66+
} else if equalSyntax(rhs2, a) || equalSyntax(rhs, b) {
67+
sign = -sign
68+
} else {
69+
continue
70+
}
71+
72+
sym := cond(sign < 0, "min", "max")
73+
74+
if _, obj := scope.LookupParent(sym, ifStmt.Pos()); !is[*types.Builtin](obj) {
75+
continue // min/max function is shadowed
76+
}
77+
78+
// pattern 1
79+
//
80+
// TODO(adonovan): if lhs is declared "var lhs T" on preceding line,
81+
// simplify the whole thing to "lhs := min(a, b)".
82+
pass.Report(analysis.Diagnostic{
83+
// Highlight the condition a < b.
84+
Pos: compare.Pos(),
85+
End: compare.End(),
86+
Category: "minmax",
87+
Message: fmt.Sprintf("if/else statement can be modernized using %s", sym),
88+
SuggestedFixes: []analysis.SuggestedFix{{
89+
Message: fmt.Sprintf("Replace if statement with %s", sym),
90+
TextEdits: []analysis.TextEdit{{
91+
// Replace IfStmt with lhs = min(a, b).
92+
Pos: ifStmt.Pos(),
93+
End: ifStmt.End(),
94+
NewText: []byte(fmt.Sprintf("%s = %s(%s, %s)",
95+
formatNode(pass.Fset, lhs),
96+
sym,
97+
formatNode(pass.Fset, a),
98+
formatNode(pass.Fset, b))),
99+
}},
100+
}},
101+
})
102+
}
103+
104+
} else if prev, ok := curIfStmt.PrevSibling(); ok && is[*ast.AssignStmt](prev.Node()) {
105+
fassign := prev.Node().(*ast.AssignStmt)
106+
107+
// Have: lhs2 = rhs2; if a < b { lhs = rhs }
108+
// For pattern 2, check that
109+
// - lhs = lhs2
110+
// - {rhs,rhs2} = {a,b}, but allow lhs2 to
111+
// stand for rhs2.
112+
// TODO(adonovan): accept "var lhs2 = rhs2" form too.
113+
lhs2 := fassign.Lhs[0]
114+
rhs2 := fassign.Rhs[0]
115+
116+
if equalSyntax(lhs, lhs2) {
117+
if equalSyntax(rhs, a) && (equalSyntax(rhs2, b) || equalSyntax(lhs2, b)) {
118+
sign = +sign
119+
} else if (equalSyntax(rhs2, a) || equalSyntax(lhs2, a)) && equalSyntax(rhs, b) {
120+
sign = -sign
121+
} else {
122+
continue
123+
}
124+
sym := cond(sign < 0, "min", "max")
125+
126+
if _, obj := scope.LookupParent(sym, ifStmt.Pos()); !is[*types.Builtin](obj) {
127+
continue // min/max function is shadowed
128+
}
129+
130+
// pattern 2
131+
pass.Report(analysis.Diagnostic{
132+
// Highlight the condition a < b.
133+
Pos: compare.Pos(),
134+
End: compare.End(),
135+
Category: "minmax",
136+
Message: fmt.Sprintf("if statement can be modernized using %s", sym),
137+
SuggestedFixes: []analysis.SuggestedFix{{
138+
Message: fmt.Sprintf("Replace if/else with %s", sym),
139+
TextEdits: []analysis.TextEdit{{
140+
// Replace rhs2 and IfStmt with min(a, b)
141+
Pos: rhs2.Pos(),
142+
End: ifStmt.End(),
143+
NewText: []byte(fmt.Sprintf("%s(%s, %s)",
144+
sym,
145+
formatNode(pass.Fset, a),
146+
formatNode(pass.Fset, b))),
147+
}},
148+
}},
149+
})
150+
}
151+
}
152+
}
153+
}
154+
}
155+
156+
// isInequality reports non-zero if tok is one of < <= => >:
157+
// +1 for > and -1 for <.
158+
func isInequality(tok token.Token) int {
159+
switch tok {
160+
case token.LEQ, token.LSS:
161+
return -1
162+
case token.GEQ, token.GTR:
163+
return +1
164+
}
165+
return 0
166+
}
167+
168+
// isAssignBlock reports whether b is a block of the form { lhs = rhs }.
169+
func isAssignBlock(b *ast.BlockStmt) bool {
170+
if len(b.List) != 1 {
171+
return false
172+
}
173+
assign, ok := b.List[0].(*ast.AssignStmt)
174+
return ok && assign.Tok == token.ASSIGN && len(assign.Lhs) == 1 && len(assign.Rhs) == 1
175+
}
176+
177+
// -- utils --
178+
179+
func is[T any](x any) bool {
180+
_, ok := x.(T)
181+
return ok
182+
}
183+
184+
func cond[T any](cond bool, t, f T) T {
185+
if cond {
186+
return t
187+
} else {
188+
return f
189+
}
190+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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 modernize
6+
7+
import (
8+
_ "embed"
9+
"go/ast"
10+
"go/format"
11+
"go/token"
12+
"strings"
13+
14+
"golang.org/x/tools/go/analysis"
15+
"golang.org/x/tools/go/analysis/passes/inspect"
16+
"golang.org/x/tools/gopls/internal/util/astutil"
17+
"golang.org/x/tools/internal/analysisinternal"
18+
)
19+
20+
//go:embed doc.go
21+
var doc string
22+
23+
var Analyzer = &analysis.Analyzer{
24+
Name: "modernize",
25+
Doc: analysisinternal.MustExtractDoc(doc, "modernize"),
26+
Requires: []*analysis.Analyzer{inspect.Analyzer},
27+
Run: run,
28+
URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize",
29+
}
30+
31+
func run(pass *analysis.Pass) (any, error) {
32+
minmax(pass)
33+
// TODO(adonovan): more modernizers here; see #70815.
34+
return nil, nil
35+
}
36+
37+
// -- helpers --
38+
39+
// equalSyntax reports whether x and y are syntactically equal (ignoring comments).
40+
func equalSyntax(x, y ast.Expr) bool {
41+
sameName := func(x, y *ast.Ident) bool { return x.Name == y.Name }
42+
return astutil.Equal(x, y, sameName)
43+
}
44+
45+
// formatNode formats n.
46+
func formatNode(fset *token.FileSet, n ast.Node) string {
47+
var buf strings.Builder
48+
format.Node(&buf, fset, n) // ignore errors
49+
return buf.String()
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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 modernize_test
6+
7+
import (
8+
"testing"
9+
10+
"golang.org/x/tools/go/analysis/analysistest"
11+
"golang.org/x/tools/gopls/internal/analysis/modernize"
12+
)
13+
14+
func Test(t *testing.T) {
15+
analysistest.RunWithSuggestedFixes(t, analysistest.TestData(), modernize.Analyzer,
16+
"minmax")
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package minmax
2+
3+
func ifmin(a, b int) {
4+
x := a
5+
if a < b { // want "if statement can be modernized using max"
6+
x = b
7+
}
8+
print(x)
9+
}
10+
11+
func ifmax(a, b int) {
12+
x := a
13+
if a > b { // want "if statement can be modernized using min"
14+
x = b
15+
}
16+
print(x)
17+
}
18+
19+
func ifminvariant(a, b int) {
20+
x := a
21+
if x > b { // want "if statement can be modernized using min"
22+
x = b
23+
}
24+
print(x)
25+
}
26+
27+
func ifmaxvariant(a, b int) {
28+
x := b
29+
if a < x { // want "if statement can be modernized using min"
30+
x = a
31+
}
32+
print(x)
33+
}
34+
35+
func ifelsemin(a, b int) {
36+
var x int
37+
if a <= b { // want "if/else statement can be modernized using min"
38+
x = a
39+
} else {
40+
x = b
41+
}
42+
print(x)
43+
}
44+
45+
func ifelsemax(a, b int) {
46+
var x int
47+
if a >= b { // want "if/else statement can be modernized using max"
48+
x = a
49+
} else {
50+
x = b
51+
}
52+
print(x)
53+
}
54+
55+
func shadowed() int {
56+
hour, min := 3600, 60
57+
58+
var time int
59+
if hour < min { // silent: the built-in min function is shadowed here
60+
time = hour
61+
} else {
62+
time = min
63+
}
64+
return time
65+
}

0 commit comments

Comments
 (0)