Skip to content

Commit e3a69ff

Browse files
adonovangopherbot
authored andcommitted
gopls/internal/golang: refactor.inline.variable: add parens
Depending on the context into which we are inlining a variable's initializer expression, parens may be needed to preserve operator precedence. We already have logic for this in internal/refactor/inline, so it has been promoted to astutil.MaybeParenthesize. + test. (In fact one existing test covered the buggy behavior!) Fixes golang/go#77381 Change-Id: I0d3d4445d444d8e048e1fdf8ff5ff16a3a2755ee Reviewed-on: https://go-review.googlesource.com/c/tools/+/740640 Reviewed-by: Madeline Kalil <mkalil@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Auto-Submit: Alan Donovan <adonovan@google.com>
1 parent 955d132 commit e3a69ff

File tree

5 files changed

+107
-73
lines changed

5 files changed

+107
-73
lines changed

gopls/internal/golang/inline.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,10 +221,11 @@ func inlineVariableOne(pkg *cache.Package, pgf *parsego.File, start, end token.P
221221
if !ok {
222222
return nil, nil, fmt.Errorf("cannot inline variable here")
223223
}
224-
use := curUse.Node().(*ast.Ident)
225224

226225
// Check that free symbols of rhs are unshadowed at curUse.
227226
var (
227+
use = curUse.Node().(*ast.Ident)
228+
rhs = curRHS.Node().(ast.Expr)
228229
pos = use.Pos()
229230
scope = info.Scopes[pgf.File].Innermost(pos)
230231
)
@@ -240,7 +241,7 @@ func inlineVariableOne(pkg *cache.Package, pgf *parsego.File, start, end token.P
240241
if v, ok := obj1.(*types.Var); ok && v.IsField() {
241242
continue // a field reference T{F: 0} is non-lexical
242243
}
243-
if astutil.NodeContainsPos(curRHS.Node(), obj1.Pos()) {
244+
if astutil.NodeContainsPos(rhs, obj1.Pos()) {
244245
continue // not free (id is defined within RHS)
245246
}
246247
_, obj2 := scope.LookupParent(id.Name, pos)
@@ -252,13 +253,16 @@ func inlineVariableOne(pkg *cache.Package, pgf *parsego.File, start, end token.P
252253

253254
// TODO(adonovan): also reject variables that are updated by assignments?
254255

256+
// Add parens to 'new' as needed by the 'use' context.
257+
rhs = astutil.MaybeParenthesize(curUse.Parent().Node(), use, rhs)
258+
255259
return pkg.FileSet(), &analysis.SuggestedFix{
256260
Message: fmt.Sprintf("Replace variable %q by its initializer expression", use.Name),
257261
TextEdits: []analysis.TextEdit{
258262
{
259263
Pos: use.Pos(),
260264
End: use.End(),
261-
NewText: []byte(FormatNode(pkg.FileSet(), curRHS.Node())),
265+
NewText: []byte(FormatNode(pkg.FileSet(), rhs)),
262266
},
263267
},
264268
}, nil

gopls/internal/test/marker/testdata/codeaction/inline-lhs-var-method.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func _() {
7171
v.PointerMethod() //@codeaction("v", "refactor.inline.variable", err="0 CodeActions of kind refactor.inline.variable")
7272
(v).PointerMethod() //@codeaction("v", "refactor.inline.variable", err="0 CodeActions of kind refactor.inline.variable")
7373
var vptr *V = &v
74-
&v.PointerMethod() //@codeaction("vptr", "refactor.inline.variable", result=inlintVptr)
74+
(&v).PointerMethod() //@codeaction("vptr", "refactor.inline.variable", result=inlintVptr)
7575
(vptr).PointerMethod() //@codeaction("vptr", "refactor.inline.variable", result=inlintVptrpar)
7676
fmt.Println(v, vptr)
7777
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
Test that refactor.inline.variable adds parens as needed to preserve
2+
operator precedence (go.dev/issue/77381).
3+
4+
-- go.mod --
5+
module example.com/a
6+
go 1.18
7+
8+
-- a/a.go --
9+
package a
10+
11+
func _() {
12+
x := 3 + 4
13+
print(x) //@codeaction("x", "refactor.inline.variable", result=one)
14+
print(2 * x) //@codeaction("x", "refactor.inline.variable", result=two)
15+
}
16+
-- @one/a/a.go --
17+
package a
18+
19+
func _() {
20+
x := 3 + 4
21+
print(3 + 4) //@codeaction("x", "refactor.inline.variable", result=one)
22+
print(2 * x) //@codeaction("x", "refactor.inline.variable", result=two)
23+
}
24+
-- @two/a/a.go --
25+
package a
26+
27+
func _() {
28+
x := 3 + 4
29+
print(x) //@codeaction("x", "refactor.inline.variable", result=one)
30+
print(2 * (3 + 4)) //@codeaction("x", "refactor.inline.variable", result=two)
31+
}

internal/astutil/util.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,68 @@ func Select(curFile inspector.Cursor, start, end token.Pos) (_enclosing, _start,
228228
}
229229

230230
var noCursor inspector.Cursor
231+
232+
// MaybeParenthesize returns new, possibly wrapped in parens if needed
233+
// to preserve operator precedence when it replaces old, whose parent
234+
// is parentNode.
235+
//
236+
// (This would be more naturally written in terms of Cursor, but one of
237+
// the callers--the inliner--does not have cursors handy.)
238+
func MaybeParenthesize(parentNode ast.Node, old, new ast.Expr) ast.Expr {
239+
if needsParens(parentNode, old, new) {
240+
new = &ast.ParenExpr{X: new}
241+
}
242+
return new
243+
}
244+
245+
func needsParens(parentNode ast.Node, old, new ast.Expr) bool {
246+
// An expression beneath a non-expression
247+
// has no precedence ambiguity.
248+
parent, ok := parentNode.(ast.Expr)
249+
if !ok {
250+
return false
251+
}
252+
253+
precedence := func(n ast.Node) int {
254+
switch n := n.(type) {
255+
case *ast.UnaryExpr, *ast.StarExpr:
256+
return token.UnaryPrec
257+
case *ast.BinaryExpr:
258+
return n.Op.Precedence()
259+
}
260+
return -1
261+
}
262+
263+
// Parens are not required if the new node
264+
// is not unary or binary.
265+
newprec := precedence(new)
266+
if newprec < 0 {
267+
return false
268+
}
269+
270+
// Parens are required if parent and child are both
271+
// unary or binary and the parent has higher precedence.
272+
if precedence(parent) > newprec {
273+
return true
274+
}
275+
276+
// Was the old node the operand of a postfix operator?
277+
// f().sel
278+
// f()[i:j]
279+
// f()[i]
280+
// f().(T)
281+
// f()(x)
282+
switch parent := parent.(type) {
283+
case *ast.SelectorExpr:
284+
return parent.X == old
285+
case *ast.IndexExpr:
286+
return parent.X == old
287+
case *ast.SliceExpr:
288+
return parent.X == old
289+
case *ast.TypeAssertExpr:
290+
return parent.X == old
291+
case *ast.CallExpr:
292+
return parent.Fun == old
293+
}
294+
return false
295+
}

internal/refactor/inline/inline.go

Lines changed: 3 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,9 @@ func (st *state) inline() (*Result, error) {
132132
// the x subtree is subject to precedence ambiguity
133133
// (replacing x by p+q would give p+q[y:z] which is wrong)
134134
// but the y and z subtrees are safe.
135-
if needsParens(caller.path, res.old, res.new) {
136-
res.new = &ast.ParenExpr{X: res.new.(ast.Expr)}
135+
if new, ok := res.new.(ast.Expr); ok {
136+
parent := caller.path[slices.Index(caller.path, res.old)+1]
137+
res.new = internalastutil.MaybeParenthesize(parent, res.old.(ast.Expr), new)
137138
}
138139

139140
// Some reduction strategies return a new block holding the
@@ -3137,73 +3138,6 @@ func last[T any](slice []T) T {
31373138
return *new(T)
31383139
}
31393140

3140-
// needsParens reports whether parens are required to avoid ambiguity
3141-
// around the new node replacing the specified old node (which is some
3142-
// ancestor of the CallExpr identified by its PathEnclosingInterval).
3143-
func needsParens(callPath []ast.Node, old, new ast.Node) bool {
3144-
// Find enclosing old node and its parent.
3145-
i := slices.Index(callPath, old)
3146-
if i == -1 {
3147-
panic("not found")
3148-
}
3149-
3150-
// There is no precedence ambiguity when replacing
3151-
// (e.g.) a statement enclosing the call.
3152-
if !is[ast.Expr](old) {
3153-
return false
3154-
}
3155-
3156-
// An expression beneath a non-expression
3157-
// has no precedence ambiguity.
3158-
parent, ok := callPath[i+1].(ast.Expr)
3159-
if !ok {
3160-
return false
3161-
}
3162-
3163-
precedence := func(n ast.Node) int {
3164-
switch n := n.(type) {
3165-
case *ast.UnaryExpr, *ast.StarExpr:
3166-
return token.UnaryPrec
3167-
case *ast.BinaryExpr:
3168-
return n.Op.Precedence()
3169-
}
3170-
return -1
3171-
}
3172-
3173-
// Parens are not required if the new node
3174-
// is not unary or binary.
3175-
newprec := precedence(new)
3176-
if newprec < 0 {
3177-
return false
3178-
}
3179-
3180-
// Parens are required if parent and child are both
3181-
// unary or binary and the parent has higher precedence.
3182-
if precedence(parent) > newprec {
3183-
return true
3184-
}
3185-
3186-
// Was the old node the operand of a postfix operator?
3187-
// f().sel
3188-
// f()[i:j]
3189-
// f()[i]
3190-
// f().(T)
3191-
// f()(x)
3192-
switch parent := parent.(type) {
3193-
case *ast.SelectorExpr:
3194-
return parent.X == old
3195-
case *ast.IndexExpr:
3196-
return parent.X == old
3197-
case *ast.SliceExpr:
3198-
return parent.X == old
3199-
case *ast.TypeAssertExpr:
3200-
return parent.X == old
3201-
case *ast.CallExpr:
3202-
return parent.Fun == old
3203-
}
3204-
return false
3205-
}
3206-
32073141
// declares returns the set of lexical names declared by a
32083142
// sequence of statements from the same block, excluding sub-blocks.
32093143
// (Lexical names do not include control labels.)

0 commit comments

Comments
 (0)