@@ -35,15 +35,16 @@ func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file
35
35
}
36
36
constant := info .Types [expr ].Value != nil
37
37
38
- // Create new AST node for extracted expression.
38
+ // Generate name(s) for new declaration.
39
+ baseName := cond (constant , "k" , "x" )
39
40
var lhsNames []string
40
41
switch expr := expr .(type ) {
41
42
case * ast.CallExpr :
42
43
tup , ok := info .TypeOf (expr ).(* types.Tuple )
43
44
if ! ok {
44
45
// conversion or single-valued call:
45
46
// treat it the same as our standard extract variable case.
46
- name , _ := generateAvailableName ( expr .Pos (), path , pkg , info , "x" , 0 )
47
+ name , _ := freshName ( info , file , expr .Pos (), baseName , 0 )
47
48
lhsNames = append (lhsNames , name )
48
49
49
50
} else {
@@ -52,14 +53,14 @@ func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file
52
53
for range tup .Len () {
53
54
// Generate a unique variable for each result.
54
55
var name string
55
- name , idx = generateAvailableName ( expr .Pos (), path , pkg , info , "x" , idx )
56
+ name , idx = freshName ( info , file , expr .Pos (), baseName , idx )
56
57
lhsNames = append (lhsNames , name )
57
58
}
58
59
}
59
60
60
61
default :
61
62
// TODO: stricter rules for selectorExpr.
62
- name , _ := generateAvailableName ( expr .Pos (), path , pkg , info , "x" , 0 )
63
+ name , _ := freshName ( info , file , expr .Pos (), baseName , 0 )
63
64
lhsNames = append (lhsNames , name )
64
65
}
65
66
@@ -87,20 +88,38 @@ func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file
87
88
// if x := 1; cond {
88
89
// } else if y := x1; cond {
89
90
// }
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
95
- insertBeforeStmt := analysisinternal .StmtToInsertVarBefore (path )
96
- if insertBeforeStmt == nil {
97
- return nil , nil , fmt .Errorf ("cannot find location to insert extraction" )
98
- }
99
- indent , err := calculateIndentation (src , tokFile , insertBeforeStmt )
100
- if err != nil {
101
- return nil , nil , err
91
+ var (
92
+ insertPos token.Pos
93
+ indentation string
94
+ stmtOK bool // ok to use ":=" instead of var/const decl?
95
+ )
96
+ if before := analysisinternal .StmtToInsertVarBefore (path ); before != nil {
97
+ // Within function: compute appropriate statement indentation.
98
+ indent , err := calculateIndentation (src , tokFile , before )
99
+ if err != nil {
100
+ return nil , nil , err
101
+ }
102
+ insertPos = before .Pos ()
103
+ indentation = "\n " + indent
104
+
105
+ // Currently, we always extract a constant expression
106
+ // to a const declaration (and logic in CodeAction
107
+ // assumes that we do so); this is conservative because
108
+ // it preserves its constant-ness.
109
+ //
110
+ // In future, constant expressions used only in
111
+ // contexts where constant-ness isn't important could
112
+ // be profitably extracted to a var declaration or :=
113
+ // statement, especially if the latter is the Init of
114
+ // an {If,For,Switch}Stmt.
115
+ stmtOK = ! constant
116
+ } else {
117
+ // Outside any statement: insert before the current
118
+ // declaration, without indentation.
119
+ currentDecl := path [len (path )- 2 ]
120
+ insertPos = currentDecl .Pos ()
121
+ indentation = "\n "
102
122
}
103
- newLineIndent := "\n " + indent
104
123
105
124
// Create statement to declare extracted var/const.
106
125
//
@@ -121,17 +140,19 @@ func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file
121
140
//
122
141
// Conversely, a short var decl stmt is not valid at top level,
123
142
// 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
- },
143
+ var newNode ast.Node
144
+ if ! stmtOK {
145
+ // var/const x1, ..., xn = expr
146
+ var names []* ast.Ident
147
+ for _ , name := range lhsNames {
148
+ names = append (names , ast .NewIdent (name ))
149
+ }
150
+ newNode = & ast.GenDecl {
151
+ Tok : cond (constant , token .CONST , token .VAR ),
152
+ Specs : []ast.Spec {
153
+ & ast.ValueSpec {
154
+ Names : names ,
155
+ Values : []ast.Expr {expr },
135
156
},
136
157
},
137
158
}
@@ -142,7 +163,7 @@ func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file
142
163
for _ , name := range lhsNames {
143
164
lhs = append (lhs , ast .NewIdent (name ))
144
165
}
145
- declStmt = & ast.AssignStmt {
166
+ newNode = & ast.AssignStmt {
146
167
Tok : token .DEFINE ,
147
168
Lhs : lhs ,
148
169
Rhs : []ast.Expr {expr },
@@ -151,17 +172,17 @@ func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file
151
172
152
173
// Format and indent the declaration.
153
174
var buf bytes.Buffer
154
- if err := format .Node (& buf , fset , declStmt ); err != nil {
175
+ if err := format .Node (& buf , fset , newNode ); err != nil {
155
176
return nil , nil , err
156
177
}
157
178
// TODO(adonovan): not sound for `...` string literals containing newlines.
158
- assignment := strings .ReplaceAll (buf .String (), "\n " , newLineIndent ) + newLineIndent
179
+ assignment := strings .ReplaceAll (buf .String (), "\n " , indentation ) + indentation
159
180
160
181
return fset , & analysis.SuggestedFix {
161
182
TextEdits : []analysis.TextEdit {
162
183
{
163
- Pos : insertBeforeStmt . Pos () ,
164
- End : insertBeforeStmt . Pos () ,
184
+ Pos : insertPos ,
185
+ End : insertPos ,
165
186
NewText : []byte (assignment ),
166
187
},
167
188
{
@@ -215,40 +236,36 @@ func calculateIndentation(content []byte, tok *token.File, insertBeforeStmt ast.
215
236
return string (content [lineOffset :stmtOffset ]), nil
216
237
}
217
238
218
- // generateAvailableName adjusts the new function name until there are no collisions in scope.
219
- // Possible collisions include other function and variable names. Returns the next index to check for prefix.
220
- func generateAvailableName ( pos token. Pos , path []ast. Node , pkg * types. Package , info * types. Info , prefix string , idx int ) ( string , int ) {
221
- scopes := CollectScopes (info , path , pos )
222
- scopes = append ( scopes , pkg . Scope () )
239
+ // freshName returns an identifier based on prefix (perhaps with a
240
+ // numeric suffix) that is not in scope at the specified position
241
+ // within the file. It returns the next numeric suffix to use.
242
+ func freshName (info * types. Info , file * ast. File , pos token. Pos , prefix string , idx int ) ( string , int ) {
243
+ scope := info . Scopes [ file ]. Innermost ( pos )
223
244
return generateName (idx , prefix , func (name string ) bool {
224
- for _ , scope := range scopes {
225
- if scope != nil && scope .Lookup (name ) != nil {
226
- return true
227
- }
228
- }
229
- return false
245
+ obj , _ := scope .LookupParent (name , pos )
246
+ return obj != nil
230
247
})
231
248
}
232
249
233
- // generateNameOutsideOfRange is like generateAvailableName , but ignores names
250
+ // freshNameOutsideRange is like [freshName] , but ignores names
234
251
// declared between start and end for the purposes of detecting conflicts.
235
252
//
236
253
// This is used for function extraction, where [start, end) will be extracted
237
254
// to a new scope.
238
- func generateNameOutsideOfRange (start , end token.Pos , path []ast.Node , pkg * types.Package , info * types.Info , prefix string , idx int ) (string , int ) {
239
- scopes := CollectScopes (info , path , start )
240
- scopes = append (scopes , pkg .Scope ())
255
+ func freshNameOutsideRange (info * types.Info , file * ast.File , pos , start , end token.Pos , prefix string , idx int ) (string , int ) {
256
+ scope := info .Scopes [file ].Innermost (pos )
241
257
return generateName (idx , prefix , func (name string ) bool {
242
- for _ , scope := range scopes {
243
- if scope != nil {
244
- if obj := scope .Lookup (name ); obj != nil {
245
- // Only report a collision if the object declaration was outside the
246
- // extracted range.
247
- if obj .Pos () < start || end <= obj .Pos () {
248
- return true
249
- }
250
- }
258
+ // Only report a collision if the object declaration
259
+ // was outside the extracted range.
260
+ for scope != nil {
261
+ obj , declScope := scope .LookupParent (name , pos )
262
+ if obj == nil {
263
+ return false // undeclared
251
264
}
265
+ if ! (start <= obj .Pos () && obj .Pos () < end ) {
266
+ return true // declared outside ignored range
267
+ }
268
+ scope = declScope .Parent ()
252
269
}
253
270
return false
254
271
})
@@ -640,7 +657,7 @@ func extractFunctionMethod(fset *token.FileSet, start, end token.Pos, src []byte
640
657
// TODO(suzmue): generate a name that does not conflict for "newMethod".
641
658
funName = "newMethod"
642
659
} else {
643
- funName , _ = generateAvailableName ( start , path , pkg , info , "newFunction" , 0 )
660
+ funName , _ = freshName ( info , file , start , "newFunction" , 0 )
644
661
}
645
662
extractedFunCall := generateFuncCall (hasNonNestedReturn , hasReturnValues , params ,
646
663
append (returns , getNames (retVars )... ), funName , sym , receiverName )
@@ -1290,7 +1307,7 @@ func generateReturnInfo(enclosing *ast.FuncType, pkg *types.Package, path []ast.
1290
1307
var cond * ast.Ident
1291
1308
if ! hasNonNestedReturns {
1292
1309
// Generate information for the added bool value.
1293
- name , _ := generateNameOutsideOfRange ( start , end , path , pkg , info , "shouldReturn" , 0 )
1310
+ name , _ := freshNameOutsideRange ( info , file , path [ 0 ]. Pos (), start , end , "shouldReturn" , 0 )
1294
1311
cond = & ast.Ident {Name : name }
1295
1312
retVars = append (retVars , & returnVariable {
1296
1313
name : cond ,
@@ -1325,7 +1342,7 @@ func generateReturnInfo(enclosing *ast.FuncType, pkg *types.Package, path []ast.
1325
1342
} else if n , ok := varNameForType (typ ); ok {
1326
1343
bestName = n
1327
1344
}
1328
- retName , idx := generateNameOutsideOfRange ( start , end , path , pkg , info , bestName , nameIdx [bestName ])
1345
+ retName , idx := freshNameOutsideRange ( info , file , path [ 0 ]. Pos (), start , end , bestName , nameIdx [bestName ])
1329
1346
nameIdx [bestName ] = idx
1330
1347
z := typesinternal .ZeroExpr (file , pkg , typ )
1331
1348
if z == nil {
@@ -1540,3 +1557,11 @@ func getDecls(retVars []*returnVariable) []*ast.Field {
1540
1557
}
1541
1558
return decls
1542
1559
}
1560
+
1561
+ func cond [T any ](cond bool , t , f T ) T {
1562
+ if cond {
1563
+ return t
1564
+ } else {
1565
+ return f
1566
+ }
1567
+ }
0 commit comments