Skip to content

Commit 77fbfae

Browse files
committed
internal/lsp: clean up some of the extract function code
This CL creates a struct that simplifies some of the extract function logic. Also, add a test for extraction with an underscore in the selection (Josh mentioned that this might not work, but it seems too). Change-Id: If917614a5824e84fb79a07def3eb75f48f10a5b9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/253277 Run-TryBot: Rebecca Stambler <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Robert Findley <[email protected]>
1 parent 93a3566 commit 77fbfae

File tree

5 files changed

+122
-65
lines changed

5 files changed

+122
-65
lines changed

internal/lsp/source/command.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ var (
131131
Title: "Extract to function",
132132
suggestedFixFn: extractFunction,
133133
appliesFn: func(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, _ *types.Package, info *types.Info) bool {
134-
_, _, _, _, _, ok, _ := canExtractFunction(fset, rng, src, file, info)
134+
_, ok, _ := canExtractFunction(fset, rng, src, file, info)
135135
return ok
136136
},
137137
}

internal/lsp/source/extract.go

Lines changed: 97 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -180,11 +180,12 @@ type returnVariable struct {
180180
// of the function and insert this call as well as the extracted function into
181181
// their proper locations.
182182
func extractFunction(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) {
183-
tok, path, rng, outer, start, ok, err := canExtractFunction(fset, rng, src, file, info)
183+
p, ok, err := canExtractFunction(fset, rng, src, file, info)
184184
if !ok {
185185
return nil, fmt.Errorf("extractFunction: cannot extract %s: %v",
186186
fset.Position(rng.Start), err)
187187
}
188+
tok, path, rng, outer, start := p.tok, p.path, p.rng, p.outer, p.start
188189
fileScope := info.Scopes[file]
189190
if fileScope == nil {
190191
return nil, fmt.Errorf("extractFunction: file scope is empty")
@@ -229,8 +230,10 @@ func extractFunction(fset *token.FileSet, rng span.Range, src []byte, file *ast.
229230
// we must determine the signature of the extracted function. We will then replace
230231
// the block with an assignment statement that calls the extracted function with
231232
// the appropriate parameters and return values.
232-
free, vars, assigned, defined := collectFreeVars(
233-
info, file, fileScope, pkgScope, rng, path[0])
233+
variables, err := collectFreeVars(info, file, fileScope, pkgScope, rng, path[0])
234+
if err != nil {
235+
return nil, err
236+
}
234237

235238
var (
236239
params, returns []ast.Expr // used when calling the extracted function
@@ -269,42 +272,38 @@ func extractFunction(fset *token.FileSet, rng span.Range, src []byte, file *ast.
269272
// variable in the extracted function. Determine the outcome(s) for each variable
270273
// based on whether it is free, altered within the selected block, and used outside
271274
// of the selected block.
272-
for _, obj := range vars {
273-
if _, ok := seenVars[obj]; ok {
275+
for _, v := range variables {
276+
if _, ok := seenVars[v.obj]; ok {
274277
continue
275278
}
276-
typ := analysisinternal.TypeExpr(fset, file, pkg, obj.Type())
279+
typ := analysisinternal.TypeExpr(fset, file, pkg, v.obj.Type())
277280
if typ == nil {
278-
return nil, fmt.Errorf("nil AST expression for type: %v", obj.Name())
281+
return nil, fmt.Errorf("nil AST expression for type: %v", v.obj.Name())
279282
}
280-
seenVars[obj] = typ
281-
identifier := ast.NewIdent(obj.Name())
283+
seenVars[v.obj] = typ
284+
identifier := ast.NewIdent(v.obj.Name())
282285
// An identifier must meet three conditions to become a return value of the
283286
// extracted function. (1) its value must be defined or reassigned within
284287
// the selection (isAssigned), (2) it must be used at least once after the
285288
// selection (isUsed), and (3) its first use after the selection
286289
// cannot be its own reassignment or redefinition (objOverriden).
287-
if obj.Parent() == nil {
290+
if v.obj.Parent() == nil {
288291
return nil, fmt.Errorf("parent nil")
289292
}
290-
isUsed, firstUseAfter :=
291-
objUsed(info, span.NewRange(fset, rng.End, obj.Parent().End()), obj)
292-
_, isAssigned := assigned[obj]
293-
_, isFree := free[obj]
294-
if isAssigned && isUsed && !varOverridden(info, firstUseAfter, obj, isFree, outer) {
293+
isUsed, firstUseAfter := objUsed(info, span.NewRange(fset, rng.End, v.obj.Parent().End()), v.obj)
294+
if v.assigned && isUsed && !varOverridden(info, firstUseAfter, v.obj, v.free, outer) {
295295
returnTypes = append(returnTypes, &ast.Field{Type: typ})
296296
returns = append(returns, identifier)
297-
if !isFree {
298-
uninitialized = append(uninitialized, obj)
299-
} else if obj.Parent().Pos() == startParent.Pos() {
297+
if !v.free {
298+
uninitialized = append(uninitialized, v.obj)
299+
} else if v.obj.Parent().Pos() == startParent.Pos() {
300300
canRedefineCount++
301301
}
302302
}
303-
_, isDefined := defined[obj]
304303
// An identifier must meet two conditions to become a parameter of the
305304
// extracted function. (1) it must be free (isFree), and (2) its first
306305
// use within the selection cannot be its own definition (isDefined).
307-
if isFree && !isDefined {
306+
if v.free && !v.defined {
308307
params = append(params, identifier)
309308
paramTypes = append(paramTypes, &ast.Field{
310309
Names: []*ast.Ident{identifier},
@@ -409,8 +408,7 @@ func extractFunction(fset *token.FileSet, rng span.Range, src []byte, file *ast.
409408
// statements in the selection. Update the type signature of the extracted
410409
// function and construct the if statement that will be inserted in the enclosing
411410
// function.
412-
retVars, ifReturn, err = generateReturnInfo(
413-
enclosing, pkg, path, file, info, fset, rng.Start)
411+
retVars, ifReturn, err = generateReturnInfo(enclosing, pkg, path, file, info, fset, rng.Start)
414412
if err != nil {
415413
return nil, err
416414
}
@@ -500,13 +498,11 @@ func extractFunction(fset *token.FileSet, rng span.Range, src []byte, file *ast.
500498
fullReplacement.Write(newFuncBuf.Bytes()) // insert the extracted function
501499

502500
return &analysis.SuggestedFix{
503-
TextEdits: []analysis.TextEdit{
504-
{
505-
Pos: outer.Pos(),
506-
End: outer.End(),
507-
NewText: []byte(fullReplacement.String()),
508-
},
509-
},
501+
TextEdits: []analysis.TextEdit{{
502+
Pos: outer.Pos(),
503+
End: outer.End(),
504+
NewText: []byte(fullReplacement.String()),
505+
}},
510506
}, nil
511507
}
512508

@@ -561,15 +557,28 @@ func findParent(start ast.Node, target ast.Node) ast.Node {
561557
return parent
562558
}
563559

560+
// variable describes the status of a variable within a selection.
561+
type variable struct {
562+
obj types.Object
563+
564+
// free reports whether the variable is a free variable, meaning it should
565+
// be a parameter to the extracted function.
566+
free bool
567+
568+
// assigned reports whether the variable is assigned to in the selection.
569+
assigned bool
570+
571+
// defined reports whether the variable is defined in the selection.
572+
defined bool
573+
}
574+
564575
// collectFreeVars maps each identifier in the given range to whether it is "free."
565576
// Given a range, a variable in that range is defined as "free" if it is declared
566577
// outside of the range and neither at the file scope nor package scope. These free
567578
// variables will be used as arguments in the extracted function. It also returns a
568579
// list of identifiers that may need to be returned by the extracted function.
569580
// Some of the code in this function has been adapted from tools/cmd/guru/freevars.go.
570-
func collectFreeVars(info *types.Info, file *ast.File, fileScope *types.Scope,
571-
pkgScope *types.Scope, rng span.Range, node ast.Node) (map[types.Object]struct{},
572-
[]types.Object, map[types.Object]struct{}, map[types.Object]struct{}) {
581+
func collectFreeVars(info *types.Info, file *ast.File, fileScope, pkgScope *types.Scope, rng span.Range, node ast.Node) ([]*variable, error) {
573582
// id returns non-nil if n denotes an object that is referenced by the span
574583
// and defined either within the span or in the lexical environment. The bool
575584
// return value acts as an indicator for where it was defined.
@@ -612,7 +621,7 @@ func collectFreeVars(info *types.Info, file *ast.File, fileScope *types.Scope,
612621
}
613622
return nil, false
614623
}
615-
free := make(map[types.Object]struct{})
624+
seen := make(map[types.Object]*variable)
616625
firstUseIn := make(map[types.Object]token.Pos)
617626
var vars []types.Object
618627
ast.Inspect(node, func(n ast.Node) bool {
@@ -630,15 +639,16 @@ func collectFreeVars(info *types.Info, file *ast.File, fileScope *types.Scope,
630639
prune = true
631640
}
632641
if obj != nil {
633-
if isFree {
634-
free[obj] = struct{}{}
642+
seen[obj] = &variable{
643+
obj: obj,
644+
free: isFree,
635645
}
646+
vars = append(vars, obj)
636647
// Find the first time that the object is used in the selection.
637648
first, ok := firstUseIn[obj]
638649
if !ok || n.Pos() < first {
639650
firstUseIn[obj] = n.Pos()
640651
}
641-
vars = append(vars, obj)
642652
if prune {
643653
return false
644654
}
@@ -657,8 +667,6 @@ func collectFreeVars(info *types.Info, file *ast.File, fileScope *types.Scope,
657667
// 3: y := 3
658668
// 4: z := x + a
659669
//
660-
assigned := make(map[types.Object]struct{})
661-
defined := make(map[types.Object]struct{})
662670
ast.Inspect(node, func(n ast.Node) bool {
663671
if n == nil {
664672
return false
@@ -677,7 +685,10 @@ func collectFreeVars(info *types.Info, file *ast.File, fileScope *types.Scope,
677685
if obj == nil {
678686
continue
679687
}
680-
assigned[obj] = struct{}{}
688+
if _, ok := seen[obj]; !ok {
689+
continue
690+
}
691+
seen[obj].assigned = true
681692
if n.Tok != token.DEFINE {
682693
continue
683694
}
@@ -697,7 +708,10 @@ func collectFreeVars(info *types.Info, file *ast.File, fileScope *types.Scope,
697708
if referencesObj(info, expr, obj) {
698709
continue
699710
}
700-
defined[obj] = struct{}{}
711+
if _, ok := seen[obj]; !ok {
712+
continue
713+
}
714+
seen[obj].defined = true
701715
break
702716
}
703717
}
@@ -717,7 +731,10 @@ func collectFreeVars(info *types.Info, file *ast.File, fileScope *types.Scope,
717731
if obj == nil {
718732
continue
719733
}
720-
assigned[obj] = struct{}{}
734+
if _, ok := seen[obj]; !ok {
735+
continue
736+
}
737+
seen[obj].assigned = true
721738
}
722739
}
723740
return false
@@ -727,12 +744,23 @@ func collectFreeVars(info *types.Info, file *ast.File, fileScope *types.Scope,
727744
} else if obj, _ := id(ident); obj == nil {
728745
return false
729746
} else {
730-
assigned[obj] = struct{}{}
747+
if _, ok := seen[obj]; !ok {
748+
return false
749+
}
750+
seen[obj].assigned = true
731751
}
732752
}
733753
return true
734754
})
735-
return free, vars, assigned, defined
755+
var variables []*variable
756+
for _, obj := range vars {
757+
v, ok := seen[obj]
758+
if !ok {
759+
return nil, fmt.Errorf("no seen types.Object for %v", obj)
760+
}
761+
variables = append(variables, v)
762+
}
763+
return variables, nil
736764
}
737765

738766
// referencesObj checks whether the given object appears in the given expression.
@@ -756,29 +784,34 @@ func referencesObj(info *types.Info, expr ast.Expr, obj types.Object) bool {
756784
return hasObj
757785
}
758786

759-
// canExtractFunction reports whether the code in the given range can be extracted to a function.
760-
func canExtractFunction(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, info *types.Info) (*token.File, []ast.Node, span.Range, *ast.FuncDecl, ast.Node, bool, error) {
787+
type fnExtractParams struct {
788+
tok *token.File
789+
path []ast.Node
790+
rng span.Range
791+
outer *ast.FuncDecl
792+
start ast.Node
793+
}
794+
795+
// canExtractFunction reports whether the code in the given range can be
796+
// extracted to a function.
797+
func canExtractFunction(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, info *types.Info) (*fnExtractParams, bool, error) {
761798
if rng.Start == rng.End {
762-
return nil, nil, span.Range{}, nil, nil, false,
763-
fmt.Errorf("start and end are equal")
799+
return nil, false, fmt.Errorf("start and end are equal")
764800
}
765801
tok := fset.File(file.Pos())
766802
if tok == nil {
767-
return nil, nil, span.Range{}, nil, nil, false,
768-
fmt.Errorf("no file for pos %v", fset.Position(file.Pos()))
803+
return nil, false, fmt.Errorf("no file for pos %v", fset.Position(file.Pos()))
769804
}
770805
rng = adjustRangeForWhitespace(rng, tok, src)
771806
path, _ := astutil.PathEnclosingInterval(file, rng.Start, rng.End)
772807
if len(path) == 0 {
773-
return nil, nil, span.Range{}, nil, nil, false,
774-
fmt.Errorf("no path enclosing interval")
808+
return nil, false, fmt.Errorf("no path enclosing interval")
775809
}
776810
// Node that encloses the selection must be a statement.
777811
// TODO: Support function extraction for an expression.
778812
_, ok := path[0].(ast.Stmt)
779813
if !ok {
780-
return nil, nil, span.Range{}, nil, nil, false,
781-
fmt.Errorf("node is not a statement")
814+
return nil, false, fmt.Errorf("node is not a statement")
782815
}
783816

784817
// Find the function declaration that encloses the selection.
@@ -790,7 +823,7 @@ func canExtractFunction(fset *token.FileSet, rng span.Range, src []byte, file *a
790823
}
791824
}
792825
if outer == nil {
793-
return nil, nil, span.Range{}, nil, nil, false, fmt.Errorf("no enclosing function")
826+
return nil, false, fmt.Errorf("no enclosing function")
794827
}
795828

796829
// Find the nodes at the start and end of the selection.
@@ -799,8 +832,8 @@ func canExtractFunction(fset *token.FileSet, rng span.Range, src []byte, file *a
799832
if n == nil {
800833
return false
801834
}
802-
// Do not override 'start' with a node that begins at the same location but is
803-
// nested further from 'outer'.
835+
// Do not override 'start' with a node that begins at the same location
836+
// but is nested further from 'outer'.
804837
if start == nil && n.Pos() == rng.Start && n.End() <= rng.End {
805838
start = n
806839
}
@@ -810,10 +843,15 @@ func canExtractFunction(fset *token.FileSet, rng span.Range, src []byte, file *a
810843
return n.Pos() <= rng.End
811844
})
812845
if start == nil || end == nil {
813-
return nil, nil, span.Range{}, nil, nil, false,
814-
fmt.Errorf("range does not map to AST nodes")
846+
return nil, false, fmt.Errorf("range does not map to AST nodes")
815847
}
816-
return tok, path, rng, outer, start, true, nil
848+
return &fnExtractParams{
849+
tok: tok,
850+
path: path,
851+
rng: rng,
852+
outer: outer,
853+
start: start,
854+
}, true, nil
817855
}
818856

819857
// objUsed checks if the object is used within the range. It returns the first occurence of

internal/lsp/testdata/lsp/primarymod/extract/extract_function/extract_args_returns.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ func _() {
55
a = 5 //@mark(exSt0, "a")
66
a = a + 2 //@mark(exEn0, "2")
77
//@extractfunc(exSt0, exEn0)
8-
b := a * 2
9-
_ = 3 + 4
8+
b := a * 2 //@mark(exB, " b")
9+
_ = 3 + 4 //@mark(exEnd, "4")
10+
//@extractfunc(exB, exEnd)
1011
}

internal/lsp/testdata/lsp/primarymod/extract/extract_function/extract_args_returns.go.golden

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ func _() {
55
a := 1
66
a = fn0(a) //@mark(exEn0, "2")
77
//@extractfunc(exSt0, exEn0)
8-
b := a * 2
9-
_ = 3 + 4
8+
b := a * 2 //@mark(exB, " b")
9+
_ = 3 + 4 //@mark(exEnd, "4")
10+
//@extractfunc(exB, exEnd)
1011
}
1112

1213
func fn0(a int) int {
@@ -15,3 +16,20 @@ func fn0(a int) int {
1516
return a
1617
}
1718

19+
-- functionextraction_extract_args_returns_8_1 --
20+
package extract
21+
22+
func _() {
23+
a := 1
24+
a = 5 //@mark(exSt0, "a")
25+
a = a + 2 //@mark(exEn0, "2")
26+
//@extractfunc(exSt0, exEn0)
27+
fn0(a) //@mark(exEnd, "4")
28+
//@extractfunc(exB, exEnd)
29+
}
30+
31+
func fn0(a int) {
32+
b := a * 2
33+
_ = 3 + 4
34+
}
35+

internal/lsp/testdata/lsp/summary.txt.golden

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ FoldingRangesCount = 2
1313
FormatCount = 6
1414
ImportCount = 8
1515
SuggestedFixCount = 38
16-
FunctionExtractionCount = 11
16+
FunctionExtractionCount = 12
1717
DefinitionsCount = 63
1818
TypeDefinitionsCount = 2
1919
HighlightsCount = 69

0 commit comments

Comments
 (0)