Skip to content

Commit 61b2408

Browse files
adonovangopherbot
authored andcommitted
gopls/internal/golang: add "Extract constant" counterpart
If the selection is a constant expression, instead of "Extract variable", gopls now offers "Extract constant", and generates a constant declaration. Regrettably, I noticed pending CL 564338 only while composing this CL description; this CL supersedes that one. Apologies for the redundant efforts. + Test, doc, relnote Fixes golang/go#37170 Updates golang/go#63852 Change-Id: I1376662b50820936de1e156413537e0bcc2292ff Reviewed-on: https://go-review.googlesource.com/c/tools/+/633257 Auto-Submit: Alan Donovan <[email protected]> Reviewed-by: Robert Findley <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent c01eead commit 61b2408

File tree

14 files changed

+220
-81
lines changed

14 files changed

+220
-81
lines changed

gopls/doc/features/transformation.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ Gopls supports the following code actions:
6969
- `source.test` (undocumented) <!-- TODO: fix that -->
7070
- [`source.addTest`](#source.addTest)
7171
- [`gopls.doc.features`](README.md), which opens gopls' index of features in a browser
72+
- [`refactor.extract.constant`](#extract)
7273
- [`refactor.extract.function`](#extract)
7374
- [`refactor.extract.method`](#extract)
7475
- [`refactor.extract.toNewFile`](#extract.toNewFile)
@@ -358,6 +359,9 @@ newly created declaration that contains the selected code:
358359
![Before extracting a var](../assets/extract-var-before.png)
359360
![After extracting a var](../assets/extract-var-after.png)
360361

362+
- **`refactor.extract.constant** does the same thing for a constant
363+
expression, introducing a local const declaration.
364+
361365
If the default name for the new declaration is already in use, gopls
362366
generates a fresh name.
363367

@@ -380,11 +384,9 @@ number of cases where it falls short, including:
380384

381385
The following Extract features are planned for 2024 but not yet supported:
382386

383-
- **Extract constant** is a variant of "Extract variable" to be
384-
offered when the expression is constant; see golang/go#37170.
385387
- **Extract parameter struct** will replace two or more parameters of a
386388
function by a struct type with one field per parameter; see golang/go#65552.
387-
<!-- TODO(adonovan): review and land https://go.dev/cl/563235. -->
389+
<!-- TODO(adonovan): review and land https://go.dev/cl/620995. -->
388390
<!-- Should this operation update all callers? That's more of a Change Signature. -->
389391
- **Extract interface for type** will create a declaration of an
390392
interface type with all the methods of the selected concrete type;

gopls/doc/release/v0.17.0.md

+6
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ or by selecting a whole declaration or multiple declarations.
3838
In order to avoid ambiguity and surprise about what to extract, some kinds
3939
of paritial selection of a declaration cannot invoke this code action.
4040

41+
## Extract constant
42+
43+
When the selection is a constant expression, gopls now offers "Extract
44+
constant" instead of "Extract variable", and generates a `const`
45+
declaration instead of a local variable.
46+
4147
## Pull diagnostics
4248

4349
When initialized with the option `"pullDiagnostics": true`, gopls will advertise support for the

gopls/internal/cmd/codeaction.go

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Valid kinds include:
5151
quickfix
5252
refactor
5353
refactor.extract
54+
refactor.extract.constant
5455
refactor.extract.function
5556
refactor.extract.method
5657
refactor.extract.toNewFile

gopls/internal/cmd/usage/codeaction.hlp

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Valid kinds include:
2222
quickfix
2323
refactor
2424
refactor.extract
25+
refactor.extract.constant
2526
refactor.extract.function
2627
refactor.extract.method
2728
refactor.extract.toNewFile

gopls/internal/golang/codeaction.go

+18-3
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ var codeActionProducers = [...]codeActionProducer{
236236
{kind: settings.RefactorExtractFunction, fn: refactorExtractFunction},
237237
{kind: settings.RefactorExtractMethod, fn: refactorExtractMethod},
238238
{kind: settings.RefactorExtractToNewFile, fn: refactorExtractToNewFile},
239+
{kind: settings.RefactorExtractConstant, fn: refactorExtractVariable, needPkg: true},
239240
{kind: settings.RefactorExtractVariable, fn: refactorExtractVariable, needPkg: true},
240241
{kind: settings.RefactorInlineCall, fn: refactorInlineCall, needPkg: true},
241242
{kind: settings.RefactorRewriteChangeQuote, fn: refactorRewriteChangeQuote},
@@ -462,11 +463,25 @@ func refactorExtractMethod(ctx context.Context, req *codeActionsRequest) error {
462463
return nil
463464
}
464465

465-
// refactorExtractVariable produces "Extract variable" code actions.
466+
// refactorExtractVariable produces "Extract variable|constant" code actions.
466467
// See [extractVariable] for command implementation.
467468
func refactorExtractVariable(ctx context.Context, req *codeActionsRequest) error {
468-
if _, _, ok, _ := canExtractVariable(req.pkg.TypesInfo(), req.pgf.File, req.start, req.end); ok {
469-
req.addApplyFixAction("Extract variable", fixExtractVariable, req.loc)
469+
info := req.pkg.TypesInfo()
470+
if expr, _, err := canExtractVariable(info, req.pgf.File, req.start, req.end); err == nil {
471+
// Offer one of refactor.extract.{constant,variable}
472+
// based on the constness of the expression; this is a
473+
// limitation of the codeActionProducers mechanism.
474+
// Beware that future evolutions of the refactorings
475+
// may make them diverge to become non-complementary,
476+
// for example because "if const x = ...; y {" is illegal.
477+
constant := info.Types[expr].Value != nil
478+
if (req.kind == settings.RefactorExtractConstant) == constant {
479+
title := "Extract variable"
480+
if constant {
481+
title = "Extract constant"
482+
}
483+
req.addApplyFixAction(title, fixExtractVariable, req.loc)
484+
}
470485
}
471486
return nil
472487
}

gopls/internal/golang/extract.go

+106-39
Original file line numberDiff line numberDiff line change
@@ -26,48 +26,72 @@ import (
2626
"golang.org/x/tools/internal/typesinternal"
2727
)
2828

29+
// extractVariable implements the refactor.extract.{variable,constant} CodeAction command.
2930
func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) {
3031
tokFile := fset.File(file.FileStart)
31-
expr, path, ok, err := canExtractVariable(info, file, start, end)
32-
if !ok {
33-
return nil, nil, fmt.Errorf("extractVariable: cannot extract %s: %v", safetoken.StartPosition(fset, start), err)
32+
expr, path, err := canExtractVariable(info, file, start, end)
33+
if err != nil {
34+
return nil, nil, fmt.Errorf("cannot extract %s: %v", safetoken.StartPosition(fset, start), err)
3435
}
36+
constant := info.Types[expr].Value != nil
3537

3638
// Create new AST node for extracted expression.
3739
var lhsNames []string
3840
switch expr := expr.(type) {
3941
case *ast.CallExpr:
4042
tup, ok := info.TypeOf(expr).(*types.Tuple)
4143
if !ok {
42-
// If the call expression only has one return value, we can treat it the
43-
// same as our standard extract variable case.
44-
lhsName, _ := generateAvailableName(expr.Pos(), path, pkg, info, "x", 0)
45-
lhsNames = append(lhsNames, lhsName)
46-
break
47-
}
48-
idx := 0
49-
for i := 0; i < tup.Len(); i++ {
50-
// Generate a unique variable for each return value.
51-
var lhsName string
52-
lhsName, idx = generateAvailableName(expr.Pos(), path, pkg, info, "x", idx)
53-
lhsNames = append(lhsNames, lhsName)
44+
// conversion or single-valued call:
45+
// treat it the same as our standard extract variable case.
46+
name, _ := generateAvailableName(expr.Pos(), path, pkg, info, "x", 0)
47+
lhsNames = append(lhsNames, name)
48+
49+
} else {
50+
// call with multiple results
51+
idx := 0
52+
for range tup.Len() {
53+
// Generate a unique variable for each result.
54+
var name string
55+
name, idx = generateAvailableName(expr.Pos(), path, pkg, info, "x", idx)
56+
lhsNames = append(lhsNames, name)
57+
}
5458
}
5559

5660
default:
5761
// TODO: stricter rules for selectorExpr.
58-
lhsName, _ := generateAvailableName(expr.Pos(), path, pkg, info, "x", 0)
59-
lhsNames = append(lhsNames, lhsName)
62+
name, _ := generateAvailableName(expr.Pos(), path, pkg, info, "x", 0)
63+
lhsNames = append(lhsNames, name)
6064
}
6165

6266
// TODO: There is a bug here: for a variable declared in a labeled
6367
// switch/for statement it returns the for/switch statement itself
64-
// which produces the below code which is a compiler error e.g.
65-
// label:
66-
// switch r1 := r() { ... break label ... }
68+
// which produces the below code which is a compiler error. e.g.
69+
// label:
70+
// switch r1 := r() { ... break label ... }
6771
// On extracting "r()" to a variable
68-
// label:
69-
// x := r()
70-
// switch r1 := x { ... break label ... } // compiler error
72+
// label:
73+
// x := r()
74+
// switch r1 := x { ... break label ... } // compiler error
75+
//
76+
// TODO(golang/go#70563): Another bug: extracting the
77+
// expression to the recommended place may cause it to migrate
78+
// across one or more declarations that it references.
79+
//
80+
// Before:
81+
// if x := 1; cond {
82+
// } else if y := «x + 2»; cond {
83+
// }
84+
//
85+
// After:
86+
// x1 := x + 2 // error: undefined x
87+
// if x := 1; cond {
88+
// } else if y := x1; cond {
89+
// }
90+
//
91+
// TODO(golang/go#70665): Another bug (or limitation): this
92+
// operation fails at top-level:
93+
// package p
94+
// var x = «1 + 2» // error
7195
insertBeforeStmt := analysisinternal.StmtToInsertVarBefore(path)
7296
if insertBeforeStmt == nil {
7397
return nil, nil, fmt.Errorf("cannot find location to insert extraction")
@@ -78,16 +102,59 @@ func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file
78102
}
79103
newLineIndent := "\n" + indent
80104

81-
lhs := strings.Join(lhsNames, ", ")
82-
assignStmt := &ast.AssignStmt{
83-
Lhs: []ast.Expr{ast.NewIdent(lhs)},
84-
Tok: token.DEFINE,
85-
Rhs: []ast.Expr{expr},
105+
// Create statement to declare extracted var/const.
106+
//
107+
// TODO(adonovan): beware the const decls are not valid short
108+
// statements, so if fixing #70563 causes
109+
// StmtToInsertVarBefore to evolve to permit declarations in
110+
// the "pre" part of an IfStmt, like so:
111+
// Before:
112+
// if cond {
113+
// } else if «1 + 2» > 0 {
114+
// }
115+
// After:
116+
// if x := 1 + 2; cond {
117+
// } else if x > 0 {
118+
// }
119+
// then it will need to become aware that this is invalid
120+
// for constants.
121+
//
122+
// Conversely, a short var decl stmt is not valid at top level,
123+
// so when we fix #70665, we'll need to use a var decl.
124+
var declStmt ast.Stmt
125+
if constant {
126+
// const x = expr
127+
declStmt = &ast.DeclStmt{
128+
Decl: &ast.GenDecl{
129+
Tok: token.CONST,
130+
Specs: []ast.Spec{
131+
&ast.ValueSpec{
132+
Names: []*ast.Ident{ast.NewIdent(lhsNames[0])}, // there can be only one
133+
Values: []ast.Expr{expr},
134+
},
135+
},
136+
},
137+
}
138+
139+
} else {
140+
// var: x1, ... xn := expr
141+
var lhs []ast.Expr
142+
for _, name := range lhsNames {
143+
lhs = append(lhs, ast.NewIdent(name))
144+
}
145+
declStmt = &ast.AssignStmt{
146+
Tok: token.DEFINE,
147+
Lhs: lhs,
148+
Rhs: []ast.Expr{expr},
149+
}
86150
}
151+
152+
// Format and indent the declaration.
87153
var buf bytes.Buffer
88-
if err := format.Node(&buf, fset, assignStmt); err != nil {
154+
if err := format.Node(&buf, fset, declStmt); err != nil {
89155
return nil, nil, err
90156
}
157+
// TODO(adonovan): not sound for `...` string literals containing newlines.
91158
assignment := strings.ReplaceAll(buf.String(), "\n", newLineIndent) + newLineIndent
92159

93160
return fset, &analysis.SuggestedFix{
@@ -100,39 +167,39 @@ func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file
100167
{
101168
Pos: start,
102169
End: end,
103-
NewText: []byte(lhs),
170+
NewText: []byte(strings.Join(lhsNames, ", ")),
104171
},
105172
},
106173
}, nil
107174
}
108175

109176
// canExtractVariable reports whether the code in the given range can be
110-
// extracted to a variable.
111-
func canExtractVariable(info *types.Info, file *ast.File, start, end token.Pos) (ast.Expr, []ast.Node, bool, error) {
177+
// extracted to a variable (or constant).
178+
func canExtractVariable(info *types.Info, file *ast.File, start, end token.Pos) (ast.Expr, []ast.Node, error) {
112179
if start == end {
113-
return nil, nil, false, fmt.Errorf("empty selection")
180+
return nil, nil, fmt.Errorf("empty selection")
114181
}
115182
path, exact := astutil.PathEnclosingInterval(file, start, end)
116183
if !exact {
117-
return nil, nil, false, fmt.Errorf("selection is not an expression")
184+
return nil, nil, fmt.Errorf("selection is not an expression")
118185
}
119186
if len(path) == 0 {
120-
return nil, nil, false, bug.Errorf("no path enclosing interval")
187+
return nil, nil, bug.Errorf("no path enclosing interval")
121188
}
122189
for _, n := range path {
123190
if _, ok := n.(*ast.ImportSpec); ok {
124-
return nil, nil, false, fmt.Errorf("cannot extract variable in an import block")
191+
return nil, nil, fmt.Errorf("cannot extract variable or constant in an import block")
125192
}
126193
}
127194
expr, ok := path[0].(ast.Expr)
128195
if !ok {
129-
return nil, nil, false, fmt.Errorf("selection is not an expression") // e.g. statement
196+
return nil, nil, fmt.Errorf("selection is not an expression") // e.g. statement
130197
}
131198
if tv, ok := info.Types[expr]; !ok || !tv.IsValue() || tv.Type == nil || tv.HasOk() {
132199
// e.g. type, builtin, x.(type), 2-valued m[k], or ill-typed
133-
return nil, nil, false, fmt.Errorf("selection is not a single-valued expression")
200+
return nil, nil, fmt.Errorf("selection is not a single-valued expression")
134201
}
135-
return expr, path, true, nil
202+
return expr, path, nil
136203
}
137204

138205
// Calculate indentation for insertion.

gopls/internal/golang/fix.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func singleFile(fixer1 singleFileFixer) fixer {
5858

5959
// Names of ApplyFix.Fix created directly by the CodeAction handler.
6060
const (
61-
fixExtractVariable = "extract_variable"
61+
fixExtractVariable = "extract_variable" // (or constant)
6262
fixExtractFunction = "extract_function"
6363
fixExtractMethod = "extract_method"
6464
fixInlineCall = "inline_call"

gopls/internal/settings/codeactionkind.go

+1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ const (
9999
RefactorInlineCall protocol.CodeActionKind = "refactor.inline.call"
100100

101101
// refactor.extract
102+
RefactorExtractConstant protocol.CodeActionKind = "refactor.extract.constant"
102103
RefactorExtractFunction protocol.CodeActionKind = "refactor.extract.function"
103104
RefactorExtractMethod protocol.CodeActionKind = "refactor.extract.method"
104105
RefactorExtractVariable protocol.CodeActionKind = "refactor.extract.variable"

gopls/internal/settings/default.go

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ func DefaultOptions(overrides ...func(*Options)) *Options {
6161
RefactorRewriteRemoveUnusedParam: true,
6262
RefactorRewriteSplitLines: true,
6363
RefactorInlineCall: true,
64+
RefactorExtractConstant: true,
6465
RefactorExtractFunction: true,
6566
RefactorExtractMethod: true,
6667
RefactorExtractVariable: true,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
This test checks the behavior of the 'extract variable/constant' code actions
2+
when the optimal place for the new declaration is within the "if" statement,
3+
like so:
4+
5+
if x := 1 + 2 or y + y ; true {
6+
} else if x > 0 {
7+
}
8+
9+
A future refactor.variable implementation that does this should avoid
10+
using a 'const' declaration, which is not legal at that location.
11+
12+
-- flags --
13+
-ignore_extra_diags
14+
15+
-- a.go --
16+
package a
17+
18+
func constant() {
19+
if true {
20+
} else if 1 + 2 > 0 { //@ codeaction("1 + 2", "refactor.extract.constant", edit=constant)
21+
}
22+
}
23+
24+
func variable(y int) {
25+
if true {
26+
} else if y + y > 0 { //@ codeaction("y + y", "refactor.extract.variable", edit=variable)
27+
}
28+
}
29+
30+
-- @constant/a.go --
31+
@@ -4 +4 @@
32+
+ const x = 1 + 2
33+
@@ -5 +6 @@
34+
- } else if 1 + 2 > 0 { //@ codeaction("1 + 2", "refactor.extract.constant", edit=constant)
35+
+ } else if x > 0 { //@ codeaction("1 + 2", "refactor.extract.constant", edit=constant)
36+
-- @variable/a.go --
37+
@@ -10 +10 @@
38+
+ x := y + y
39+
@@ -11 +12 @@
40+
- } else if y + y > 0 { //@ codeaction("y + y", "refactor.extract.variable", edit=variable)
41+
+ } else if x > 0 { //@ codeaction("y + y", "refactor.extract.variable", edit=variable)

0 commit comments

Comments
 (0)