@@ -35,15 +35,16 @@ func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file
3535 }
3636 constant := info .Types [expr ].Value != nil
3737
38- // Create new AST node for extracted expression.
38+ // Generate name(s) for new declaration.
39+ baseName := cond (constant , "k" , "x" )
3940 var lhsNames []string
4041 switch expr := expr .(type ) {
4142 case * ast.CallExpr :
4243 tup , ok := info .TypeOf (expr ).(* types.Tuple )
4344 if ! ok {
4445 // conversion or single-valued call:
4546 // 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 )
4748 lhsNames = append (lhsNames , name )
4849
4950 } else {
@@ -52,14 +53,14 @@ func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file
5253 for range tup .Len () {
5354 // Generate a unique variable for each result.
5455 var name string
55- name , idx = generateAvailableName ( expr .Pos (), path , pkg , info , "x" , idx )
56+ name , idx = freshName ( info , file , expr .Pos (), baseName , idx )
5657 lhsNames = append (lhsNames , name )
5758 }
5859 }
5960
6061 default :
6162 // TODO: stricter rules for selectorExpr.
62- name , _ := generateAvailableName ( expr .Pos (), path , pkg , info , "x" , 0 )
63+ name , _ := freshName ( info , file , expr .Pos (), baseName , 0 )
6364 lhsNames = append (lhsNames , name )
6465 }
6566
@@ -87,20 +88,38 @@ func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file
8788 // if x := 1; cond {
8889 // } else if y := x1; cond {
8990 // }
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 "
102122 }
103- newLineIndent := "\n " + indent
104123
105124 // Create statement to declare extracted var/const.
106125 //
@@ -121,17 +140,19 @@ func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file
121140 //
122141 // Conversely, a short var decl stmt is not valid at top level,
123142 // 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 },
135156 },
136157 },
137158 }
@@ -142,7 +163,7 @@ func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file
142163 for _ , name := range lhsNames {
143164 lhs = append (lhs , ast .NewIdent (name ))
144165 }
145- declStmt = & ast.AssignStmt {
166+ newNode = & ast.AssignStmt {
146167 Tok : token .DEFINE ,
147168 Lhs : lhs ,
148169 Rhs : []ast.Expr {expr },
@@ -151,17 +172,17 @@ func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file
151172
152173 // Format and indent the declaration.
153174 var buf bytes.Buffer
154- if err := format .Node (& buf , fset , declStmt ); err != nil {
175+ if err := format .Node (& buf , fset , newNode ); err != nil {
155176 return nil , nil , err
156177 }
157178 // 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
159180
160181 return fset , & analysis.SuggestedFix {
161182 TextEdits : []analysis.TextEdit {
162183 {
163- Pos : insertBeforeStmt . Pos () ,
164- End : insertBeforeStmt . Pos () ,
184+ Pos : insertPos ,
185+ End : insertPos ,
165186 NewText : []byte (assignment ),
166187 },
167188 {
@@ -215,40 +236,36 @@ func calculateIndentation(content []byte, tok *token.File, insertBeforeStmt ast.
215236 return string (content [lineOffset :stmtOffset ]), nil
216237}
217238
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 )
223244 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
230247 })
231248}
232249
233- // generateNameOutsideOfRange is like generateAvailableName , but ignores names
250+ // freshNameOutsideRange is like [freshName] , but ignores names
234251// declared between start and end for the purposes of detecting conflicts.
235252//
236253// This is used for function extraction, where [start, end) will be extracted
237254// 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 )
241257 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
251264 }
265+ if ! (start <= obj .Pos () && obj .Pos () < end ) {
266+ return true // declared outside ignored range
267+ }
268+ scope = declScope .Parent ()
252269 }
253270 return false
254271 })
@@ -640,7 +657,7 @@ func extractFunctionMethod(fset *token.FileSet, start, end token.Pos, src []byte
640657 // TODO(suzmue): generate a name that does not conflict for "newMethod".
641658 funName = "newMethod"
642659 } else {
643- funName , _ = generateAvailableName ( start , path , pkg , info , "newFunction" , 0 )
660+ funName , _ = freshName ( info , file , start , "newFunction" , 0 )
644661 }
645662 extractedFunCall := generateFuncCall (hasNonNestedReturn , hasReturnValues , params ,
646663 append (returns , getNames (retVars )... ), funName , sym , receiverName )
@@ -1290,7 +1307,7 @@ func generateReturnInfo(enclosing *ast.FuncType, pkg *types.Package, path []ast.
12901307 var cond * ast.Ident
12911308 if ! hasNonNestedReturns {
12921309 // 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 )
12941311 cond = & ast.Ident {Name : name }
12951312 retVars = append (retVars , & returnVariable {
12961313 name : cond ,
@@ -1325,7 +1342,7 @@ func generateReturnInfo(enclosing *ast.FuncType, pkg *types.Package, path []ast.
13251342 } else if n , ok := varNameForType (typ ); ok {
13261343 bestName = n
13271344 }
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 ])
13291346 nameIdx [bestName ] = idx
13301347 z := typesinternal .ZeroExpr (file , pkg , typ )
13311348 if z == nil {
@@ -1540,3 +1557,11 @@ func getDecls(retVars []*returnVariable) []*ast.Field {
15401557 }
15411558 return decls
15421559}
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