Skip to content

Commit 84e9c33

Browse files
findleyrgopherbot
authored andcommitted
gopls/internal/golang: more idiomatic result naming in extract
Use field names or type names for result variables names, if available. Otherwise, use the same conventions for var naming as completion. If all else fails, use 'result' rather than 'returnValue'. For golang/go#66289 Change-Id: Ife1d5435f00d2c4930fad48e7373e987668139ef Reviewed-on: https://go-review.googlesource.com/c/tools/+/627775 Auto-Submit: Robert Findley <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Hongxiang Jiang <[email protected]>
1 parent 8bb5da3 commit 84e9c33

File tree

5 files changed

+204
-66
lines changed

5 files changed

+204
-66
lines changed

gopls/internal/golang/completion/literal.go

+3-26
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,8 @@ func (c *completer) functionLiteral(ctx context.Context, sig *types.Signature, m
370370

371371
// conventionalAcronyms contains conventional acronyms for type names
372372
// in lower case. For example, "ctx" for "context" and "err" for "error".
373+
//
374+
// Keep this up to date with golang.conventionalVarNames.
373375
var conventionalAcronyms = map[string]string{
374376
"context": "ctx",
375377
"error": "err",
@@ -382,11 +384,6 @@ var conventionalAcronyms = map[string]string{
382384
// non-identifier runes. For example, "[]int" becomes "i", and
383385
// "struct { i int }" becomes "s".
384386
func abbreviateTypeName(s string) string {
385-
var (
386-
b strings.Builder
387-
useNextUpper bool
388-
)
389-
390387
// Trim off leading non-letters. We trim everything between "[" and
391388
// "]" to handle array types like "[someConst]int".
392389
var inBracket bool
@@ -407,27 +404,7 @@ func abbreviateTypeName(s string) string {
407404
return acr
408405
}
409406

410-
for i, r := range s {
411-
// Stop if we encounter a non-identifier rune.
412-
if !unicode.IsLetter(r) && !unicode.IsNumber(r) {
413-
break
414-
}
415-
416-
if i == 0 {
417-
b.WriteRune(unicode.ToLower(r))
418-
}
419-
420-
if unicode.IsUpper(r) {
421-
if useNextUpper {
422-
b.WriteRune(unicode.ToLower(r))
423-
useNextUpper = false
424-
}
425-
} else {
426-
useNextUpper = true
427-
}
428-
}
429-
430-
return b.String()
407+
return golang.AbbreviateVarName(s)
431408
}
432409

433410
// compositeLiteral adds a composite literal completion item for the given typeName.

gopls/internal/golang/extract.go

+105-25
Original file line numberDiff line numberDiff line change
@@ -37,22 +37,22 @@ func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file
3737
// TODO: stricter rules for selectorExpr.
3838
case *ast.BasicLit, *ast.CompositeLit, *ast.IndexExpr, *ast.SliceExpr,
3939
*ast.UnaryExpr, *ast.BinaryExpr, *ast.SelectorExpr:
40-
lhsName, _ := generateAvailableIdentifier(expr.Pos(), path, pkg, info, "x", 0)
40+
lhsName, _ := generateAvailableName(expr.Pos(), path, pkg, info, "x", 0)
4141
lhsNames = append(lhsNames, lhsName)
4242
case *ast.CallExpr:
4343
tup, ok := info.TypeOf(expr).(*types.Tuple)
4444
if !ok {
4545
// If the call expression only has one return value, we can treat it the
4646
// same as our standard extract variable case.
47-
lhsName, _ := generateAvailableIdentifier(expr.Pos(), path, pkg, info, "x", 0)
47+
lhsName, _ := generateAvailableName(expr.Pos(), path, pkg, info, "x", 0)
4848
lhsNames = append(lhsNames, lhsName)
4949
break
5050
}
5151
idx := 0
5252
for i := 0; i < tup.Len(); i++ {
5353
// Generate a unique variable for each return value.
5454
var lhsName string
55-
lhsName, idx = generateAvailableIdentifier(expr.Pos(), path, pkg, info, "x", idx)
55+
lhsName, idx = generateAvailableName(expr.Pos(), path, pkg, info, "x", idx)
5656
lhsNames = append(lhsNames, lhsName)
5757
}
5858
default:
@@ -150,12 +150,12 @@ func calculateIndentation(content []byte, tok *token.File, insertBeforeStmt ast.
150150
return string(content[lineOffset:stmtOffset]), nil
151151
}
152152

153-
// generateAvailableIdentifier adjusts the new function name until there are no collisions in scope.
153+
// generateAvailableName adjusts the new function name until there are no collisions in scope.
154154
// Possible collisions include other function and variable names. Returns the next index to check for prefix.
155-
func generateAvailableIdentifier(pos token.Pos, path []ast.Node, pkg *types.Package, info *types.Info, prefix string, idx int) (string, int) {
155+
func generateAvailableName(pos token.Pos, path []ast.Node, pkg *types.Package, info *types.Info, prefix string, idx int) (string, int) {
156156
scopes := CollectScopes(info, path, pos)
157157
scopes = append(scopes, pkg.Scope())
158-
return generateIdentifier(idx, prefix, func(name string) bool {
158+
return generateName(idx, prefix, func(name string) bool {
159159
for _, scope := range scopes {
160160
if scope != nil && scope.Lookup(name) != nil {
161161
return true
@@ -165,7 +165,31 @@ func generateAvailableIdentifier(pos token.Pos, path []ast.Node, pkg *types.Pack
165165
})
166166
}
167167

168-
func generateIdentifier(idx int, prefix string, hasCollision func(string) bool) (string, int) {
168+
// generateNameOutsideOfRange is like generateAvailableName, but ignores names
169+
// declared between start and end for the purposes of detecting conflicts.
170+
//
171+
// This is used for function extraction, where [start, end) will be extracted
172+
// to a new scope.
173+
func generateNameOutsideOfRange(start, end token.Pos, path []ast.Node, pkg *types.Package, info *types.Info, prefix string, idx int) (string, int) {
174+
scopes := CollectScopes(info, path, start)
175+
scopes = append(scopes, pkg.Scope())
176+
return generateName(idx, prefix, func(name string) bool {
177+
for _, scope := range scopes {
178+
if scope != nil {
179+
if obj := scope.Lookup(name); obj != nil {
180+
// Only report a collision if the object declaration was outside the
181+
// extracted range.
182+
if obj.Pos() < start || end <= obj.Pos() {
183+
return true
184+
}
185+
}
186+
}
187+
}
188+
return false
189+
})
190+
}
191+
192+
func generateName(idx int, prefix string, hasCollision func(string) bool) (string, int) {
169193
name := prefix
170194
if idx != 0 {
171195
name += fmt.Sprintf("%d", idx)
@@ -182,7 +206,7 @@ func generateIdentifier(idx int, prefix string, hasCollision func(string) bool)
182206
type returnVariable struct {
183207
// name is the identifier that is used on the left-hand side of the call to
184208
// the extracted function.
185-
name ast.Expr
209+
name *ast.Ident
186210
// decl is the declaration of the variable. It is used in the type signature of the
187211
// extracted function and for variable declarations.
188212
decl *ast.Field
@@ -517,7 +541,7 @@ func extractFunctionMethod(fset *token.FileSet, start, end token.Pos, src []byte
517541
// statements in the selection. Update the type signature of the extracted
518542
// function and construct the if statement that will be inserted in the enclosing
519543
// function.
520-
retVars, ifReturn, err = generateReturnInfo(enclosing, pkg, path, file, info, start, hasNonNestedReturn)
544+
retVars, ifReturn, err = generateReturnInfo(enclosing, pkg, path, file, info, start, end, hasNonNestedReturn)
521545
if err != nil {
522546
return nil, nil, err
523547
}
@@ -552,7 +576,7 @@ func extractFunctionMethod(fset *token.FileSet, start, end token.Pos, src []byte
552576
funName = name
553577
} else {
554578
name = "newFunction"
555-
funName, _ = generateAvailableIdentifier(start, path, pkg, info, name, 0)
579+
funName, _ = generateAvailableName(start, path, pkg, info, name, 0)
556580
}
557581
extractedFunCall := generateFuncCall(hasNonNestedReturn, hasReturnValues, params,
558582
append(returns, getNames(retVars)...), funName, sym, receiverName)
@@ -1187,12 +1211,12 @@ func parseBlockStmt(fset *token.FileSet, src []byte) (*ast.BlockStmt, error) {
11871211
// signature of the extracted function. We prepare names, signatures, and "zero values" that
11881212
// represent the new variables. We also use this information to construct the if statement that
11891213
// is inserted below the call to the extracted function.
1190-
func generateReturnInfo(enclosing *ast.FuncType, pkg *types.Package, path []ast.Node, file *ast.File, info *types.Info, pos token.Pos, hasNonNestedReturns bool) ([]*returnVariable, *ast.IfStmt, error) {
1214+
func generateReturnInfo(enclosing *ast.FuncType, pkg *types.Package, path []ast.Node, file *ast.File, info *types.Info, start, end token.Pos, hasNonNestedReturns bool) ([]*returnVariable, *ast.IfStmt, error) {
11911215
var retVars []*returnVariable
11921216
var cond *ast.Ident
11931217
if !hasNonNestedReturns {
11941218
// Generate information for the added bool value.
1195-
name, _ := generateAvailableIdentifier(pos, path, pkg, info, "shouldReturn", 0)
1219+
name, _ := generateNameOutsideOfRange(start, end, path, pkg, info, "shouldReturn", 0)
11961220
cond = &ast.Ident{Name: name}
11971221
retVars = append(retVars, &returnVariable{
11981222
name: cond,
@@ -1202,7 +1226,7 @@ func generateReturnInfo(enclosing *ast.FuncType, pkg *types.Package, path []ast.
12021226
}
12031227
// Generate information for the values in the return signature of the enclosing function.
12041228
if enclosing.Results != nil {
1205-
idx := 0
1229+
nameIdx := make(map[string]int) // last integral suffixes of generated names
12061230
for _, field := range enclosing.Results.List {
12071231
typ := info.TypeOf(field.Type)
12081232
if typ == nil {
@@ -1213,17 +1237,32 @@ func generateReturnInfo(enclosing *ast.FuncType, pkg *types.Package, path []ast.
12131237
if expr == nil {
12141238
return nil, nil, fmt.Errorf("nil AST expression")
12151239
}
1216-
var name string
1217-
name, idx = generateAvailableIdentifier(pos, path, pkg, info, "returnValue", idx)
1218-
z := analysisinternal.ZeroValue(file, pkg, typ)
1219-
if z == nil {
1220-
return nil, nil, fmt.Errorf("can't generate zero value for %T", typ)
1240+
names := []string{""}
1241+
if len(field.Names) > 0 {
1242+
names = nil
1243+
for _, n := range field.Names {
1244+
names = append(names, n.Name)
1245+
}
1246+
}
1247+
for _, name := range names {
1248+
bestName := "result"
1249+
if name != "" && name != "_" {
1250+
bestName = name
1251+
} else if n, ok := varNameForType(typ); ok {
1252+
bestName = n
1253+
}
1254+
retName, idx := generateNameOutsideOfRange(start, end, path, pkg, info, bestName, nameIdx[bestName])
1255+
nameIdx[bestName] = idx
1256+
z := analysisinternal.ZeroValue(file, pkg, typ)
1257+
if z == nil {
1258+
return nil, nil, fmt.Errorf("can't generate zero value for %T", typ)
1259+
}
1260+
retVars = append(retVars, &returnVariable{
1261+
name: ast.NewIdent(retName),
1262+
decl: &ast.Field{Type: expr},
1263+
zeroVal: z,
1264+
})
12211265
}
1222-
retVars = append(retVars, &returnVariable{
1223-
name: ast.NewIdent(name),
1224-
decl: &ast.Field{Type: expr},
1225-
zeroVal: z,
1226-
})
12271266
}
12281267
}
12291268
var ifReturn *ast.IfStmt
@@ -1240,6 +1279,48 @@ func generateReturnInfo(enclosing *ast.FuncType, pkg *types.Package, path []ast.
12401279
return retVars, ifReturn, nil
12411280
}
12421281

1282+
type objKey struct{ pkg, name string }
1283+
1284+
// conventionalVarNames specifies conventional names for variables with various
1285+
// standard library types.
1286+
//
1287+
// Keep this up to date with completion.conventionalAcronyms.
1288+
//
1289+
// TODO(rfindley): consider factoring out a "conventions" library.
1290+
var conventionalVarNames = map[objKey]string{
1291+
{"", "error"}: "err",
1292+
{"context", "Context"}: "ctx",
1293+
{"sql", "Tx"}: "tx",
1294+
{"http", "ResponseWriter"}: "rw", // Note: same as [AbbreviateVarName].
1295+
}
1296+
1297+
// varNameForTypeName chooses a "good" name for a variable with the given type,
1298+
// if possible. Otherwise, it returns "", false.
1299+
//
1300+
// For special types, it uses known conventional names.
1301+
func varNameForType(t types.Type) (string, bool) {
1302+
var typeName string
1303+
if tn, ok := t.(interface{ Obj() *types.TypeName }); ok {
1304+
obj := tn.Obj()
1305+
k := objKey{name: obj.Name()}
1306+
if obj.Pkg() != nil {
1307+
k.pkg = obj.Pkg().Name()
1308+
}
1309+
if name, ok := conventionalVarNames[k]; ok {
1310+
return name, true
1311+
}
1312+
typeName = obj.Name()
1313+
} else if b, ok := t.(*types.Basic); ok {
1314+
typeName = b.Name()
1315+
}
1316+
1317+
if typeName == "" {
1318+
return "", false
1319+
}
1320+
1321+
return AbbreviateVarName(typeName), true
1322+
}
1323+
12431324
// adjustReturnStatements adds "zero values" of the given types to each return statement
12441325
// in the given AST node.
12451326
func adjustReturnStatements(returnTypes []*ast.Field, seenVars map[types.Object]ast.Expr, file *ast.File, pkg *types.Package, extractedBlock *ast.BlockStmt) error {
@@ -1346,9 +1427,8 @@ func initializeVars(uninitialized []types.Object, retVars []*returnVariable, see
13461427
// Each variable added from a return statement in the selection
13471428
// must be initialized.
13481429
for i, retVar := range retVars {
1349-
n := retVar.name.(*ast.Ident)
13501430
valSpec := &ast.ValueSpec{
1351-
Names: []*ast.Ident{n},
1431+
Names: []*ast.Ident{retVar.name},
13521432
Type: retVars[i].decl.Type,
13531433
}
13541434
genDecl := &ast.GenDecl{

gopls/internal/golang/util.go

+34
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"go/types"
1313
"regexp"
1414
"strings"
15+
"unicode"
1516

1617
"golang.org/x/tools/gopls/internal/cache"
1718
"golang.org/x/tools/gopls/internal/cache/metadata"
@@ -363,3 +364,36 @@ func btoi(b bool) int {
363364
return 0
364365
}
365366
}
367+
368+
// AbbreviateVarName returns an abbreviated var name based on the given full
369+
// name (which may be a type name, for example).
370+
//
371+
// See the simple heuristics documented in line.
372+
func AbbreviateVarName(s string) string {
373+
var (
374+
b strings.Builder
375+
useNextUpper bool
376+
)
377+
for i, r := range s {
378+
// Stop if we encounter a non-identifier rune.
379+
if !unicode.IsLetter(r) && !unicode.IsNumber(r) {
380+
break
381+
}
382+
383+
// Otherwise, take the first letter from word boundaries, assuming
384+
// camelCase.
385+
if i == 0 {
386+
b.WriteRune(unicode.ToLower(r))
387+
}
388+
389+
if unicode.IsUpper(r) {
390+
if useNextUpper {
391+
b.WriteRune(unicode.ToLower(r))
392+
useNextUpper = false
393+
}
394+
} else {
395+
useNextUpper = true
396+
}
397+
}
398+
return b.String()
399+
}

gopls/internal/test/marker/testdata/codeaction/functionextraction.txt

+8-8
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ package extract
5656
func _() bool {
5757
x := 1
5858
//@codeaction("if", ifend, "refactor.extract.function", return)
59-
shouldReturn, returnValue := newFunction(x)
59+
shouldReturn, b := newFunction(x)
6060
if shouldReturn {
61-
return returnValue
61+
return b
6262
} //@loc(ifend, "}")
6363
return false
6464
}
@@ -124,9 +124,9 @@ func _() (int, string, error) {
124124
x := 1
125125
y := "hello"
126126
//@codeaction("z", rcEnd, "refactor.extract.function", rc)
127-
z, shouldReturn, returnValue, returnValue1, returnValue2 := newFunction(y, x)
127+
z, shouldReturn, i, s, err := newFunction(y, x)
128128
if shouldReturn {
129-
return returnValue, returnValue1, returnValue2
129+
return i, s, err
130130
} //@loc(rcEnd, "}")
131131
return x, z, nil
132132
}
@@ -205,9 +205,9 @@ import "go/ast"
205205
func _() {
206206
ast.Inspect(ast.NewIdent("a"), func(n ast.Node) bool {
207207
//@codeaction("if", rflEnd, "refactor.extract.function", rfl)
208-
shouldReturn, returnValue := newFunction(n)
208+
shouldReturn, b := newFunction(n)
209209
if shouldReturn {
210-
return returnValue
210+
return b
211211
} //@loc(rflEnd, "}")
212212
return false
213213
})
@@ -272,9 +272,9 @@ package extract
272272
func _() string {
273273
x := 1
274274
//@codeaction("if", riEnd, "refactor.extract.function", ri)
275-
shouldReturn, returnValue := newFunction(x)
275+
shouldReturn, s := newFunction(x)
276276
if shouldReturn {
277-
return returnValue
277+
return s
278278
} //@loc(riEnd, "}")
279279
x = 2
280280
return "b"

0 commit comments

Comments
 (0)