Skip to content

Commit 85a3833

Browse files
committed
internal/analysis/gofix: simple type aliases
Offer to replace inlinable type aliases whose RHS is a named type. Despite the amount of new code, there is little to test, because most of it duplicates constants. When there is a chain of inlinable type aliases, as in: type A = T type AA = A var v AA we don't follow the chain to the end. Instead, the first set of fixes produces: type A = T type AA = T var v A That is, the replacements happen "in parallel." A second round of fixes would rewrite var v A to var v T This case is rare enough that it's not worth doing better. For golang/go#32816. Change-Id: Ib6854d60b26a273b592a297cb9a650a31e094392 Reviewed-on: https://go-review.googlesource.com/c/tools/+/649055 Reviewed-by: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 809cde4 commit 85a3833

File tree

5 files changed

+225
-73
lines changed

5 files changed

+225
-73
lines changed

gopls/internal/analysis/gofix/gofix.go

+191-73
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,11 @@ func run(pass *analysis.Pass) (any, error) {
6363
// Pass 1: find functions and constants annotated with an appropriate "//go:fix"
6464
// comment (the syntax proposed by #32816),
6565
// and export a fact for each one.
66-
inlinableFuncs := make(map[*types.Func]*inline.Callee) // memoization of fact import (nil => no fact)
67-
inlinableConsts := make(map[*types.Const]*goFixInlineConstFact)
66+
var (
67+
inlinableFuncs = make(map[*types.Func]*inline.Callee) // memoization of fact import (nil => no fact)
68+
inlinableConsts = make(map[*types.Const]*goFixInlineConstFact)
69+
inlinableAliases = make(map[*types.TypeName]*goFixInlineAliasFact)
70+
)
6871

6972
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
7073
nodeFilter := []ast.Node{(*ast.FuncDecl)(nil), (*ast.GenDecl)(nil)}
@@ -89,52 +92,96 @@ func run(pass *analysis.Pass) (any, error) {
8992
inlinableFuncs[fn] = callee
9093

9194
case *ast.GenDecl:
92-
if decl.Tok != token.CONST {
95+
if decl.Tok != token.CONST && decl.Tok != token.TYPE {
9396
return
9497
}
9598
declInline := hasFixInline(decl.Doc)
9699
// Accept inline directives on the entire decl as well as individual specs.
97100
for _, spec := range decl.Specs {
98-
spec := spec.(*ast.ValueSpec) // guaranteed by Tok == CONST
99-
specInline := hasFixInline(spec.Doc)
100-
if declInline || specInline {
101-
for i, name := range spec.Names {
102-
if i >= len(spec.Values) {
103-
// Possible following an iota.
104-
break
105-
}
106-
val := spec.Values[i]
107-
var rhsID *ast.Ident
108-
switch e := val.(type) {
109-
case *ast.Ident:
110-
// Constants defined with the predeclared iota cannot be inlined.
111-
if pass.TypesInfo.Uses[e] == builtinIota {
112-
pass.Reportf(val.Pos(), "invalid //go:fix inline directive: const value is iota")
101+
switch spec := spec.(type) {
102+
case *ast.TypeSpec: // Tok == TYPE
103+
if !declInline && !hasFixInline(spec.Doc) {
104+
continue
105+
}
106+
if !spec.Assign.IsValid() {
107+
pass.Reportf(spec.Pos(), "invalid //go:fix inline directive: not a type alias")
108+
continue
109+
}
110+
if spec.TypeParams != nil {
111+
// TODO(jba): handle generic aliases
112+
continue
113+
}
114+
// The alias must refer to another named type.
115+
// TODO(jba): generalize to more type expressions.
116+
var rhsID *ast.Ident
117+
switch e := ast.Unparen(spec.Type).(type) {
118+
case *ast.Ident:
119+
rhsID = e
120+
case *ast.SelectorExpr:
121+
rhsID = e.Sel
122+
default:
123+
continue
124+
}
125+
lhs := pass.TypesInfo.Defs[spec.Name].(*types.TypeName)
126+
// more (jba): test one alias pointing to another alias
127+
rhs := pass.TypesInfo.Uses[rhsID].(*types.TypeName)
128+
typ := &goFixInlineAliasFact{
129+
RHSName: rhs.Name(),
130+
RHSPkgName: rhs.Pkg().Name(),
131+
RHSPkgPath: rhs.Pkg().Path(),
132+
}
133+
if rhs.Pkg() == pass.Pkg {
134+
typ.rhsObj = rhs
135+
}
136+
inlinableAliases[lhs] = typ
137+
// Create a fact only if the LHS is exported and defined at top level.
138+
// We create a fact even if the RHS is non-exported,
139+
// so we can warn about uses in other packages.
140+
if lhs.Exported() && typesinternal.IsPackageLevel(lhs) {
141+
pass.ExportObjectFact(lhs, typ)
142+
}
143+
144+
case *ast.ValueSpec: // Tok == CONST
145+
specInline := hasFixInline(spec.Doc)
146+
if declInline || specInline {
147+
for i, name := range spec.Names {
148+
if i >= len(spec.Values) {
149+
// Possible following an iota.
150+
break
151+
}
152+
val := spec.Values[i]
153+
var rhsID *ast.Ident
154+
switch e := val.(type) {
155+
case *ast.Ident:
156+
// Constants defined with the predeclared iota cannot be inlined.
157+
if pass.TypesInfo.Uses[e] == builtinIota {
158+
pass.Reportf(val.Pos(), "invalid //go:fix inline directive: const value is iota")
159+
continue
160+
}
161+
rhsID = e
162+
case *ast.SelectorExpr:
163+
rhsID = e.Sel
164+
default:
165+
pass.Reportf(val.Pos(), "invalid //go:fix inline directive: const value is not the name of another constant")
113166
continue
114167
}
115-
rhsID = e
116-
case *ast.SelectorExpr:
117-
rhsID = e.Sel
118-
default:
119-
pass.Reportf(val.Pos(), "invalid //go:fix inline directive: const value is not the name of another constant")
120-
continue
121-
}
122-
lhs := pass.TypesInfo.Defs[name].(*types.Const)
123-
rhs := pass.TypesInfo.Uses[rhsID].(*types.Const) // must be so in a well-typed program
124-
con := &goFixInlineConstFact{
125-
RHSName: rhs.Name(),
126-
RHSPkgName: rhs.Pkg().Name(),
127-
RHSPkgPath: rhs.Pkg().Path(),
128-
}
129-
if rhs.Pkg() == pass.Pkg {
130-
con.rhsObj = rhs
131-
}
132-
inlinableConsts[lhs] = con
133-
// Create a fact only if the LHS is exported and defined at top level.
134-
// We create a fact even if the RHS is non-exported,
135-
// so we can warn uses in other packages.
136-
if lhs.Exported() && typesinternal.IsPackageLevel(lhs) {
137-
pass.ExportObjectFact(lhs, con)
168+
lhs := pass.TypesInfo.Defs[name].(*types.Const)
169+
rhs := pass.TypesInfo.Uses[rhsID].(*types.Const) // must be so in a well-typed program
170+
con := &goFixInlineConstFact{
171+
RHSName: rhs.Name(),
172+
RHSPkgName: rhs.Pkg().Name(),
173+
RHSPkgPath: rhs.Pkg().Path(),
174+
}
175+
if rhs.Pkg() == pass.Pkg {
176+
con.rhsObj = rhs
177+
}
178+
inlinableConsts[lhs] = con
179+
// Create a fact only if the LHS is exported and defined at top level.
180+
// We create a fact even if the RHS is non-exported,
181+
// so we can warn about uses in other packages.
182+
if lhs.Exported() && typesinternal.IsPackageLevel(lhs) {
183+
pass.ExportObjectFact(lhs, con)
184+
}
138185
}
139186
}
140187
}
@@ -143,7 +190,7 @@ func run(pass *analysis.Pass) (any, error) {
143190
})
144191

145192
// Pass 2. Inline each static call to an inlinable function
146-
// and each reference to an inlinable constant.
193+
// and each reference to an inlinable constant or type alias.
147194
//
148195
// TODO(adonovan): handle multiple diffs that each add the same import.
149196
for cur := range cursor.Root(inspect).Preorder((*ast.CallExpr)(nil), (*ast.Ident)(nil)) {
@@ -218,6 +265,65 @@ func run(pass *analysis.Pass) (any, error) {
218265
}
219266

220267
case *ast.Ident:
268+
// If the identifier is a use of an inlinable type alias, suggest inlining it.
269+
// TODO(jba): much of this code is shared with the constant case, below.
270+
// Try to factor more of it out, unless it will change anyway when we move beyond simple RHS's.
271+
if ali, ok := pass.TypesInfo.Uses[n].(*types.TypeName); ok {
272+
inalias, ok := inlinableAliases[ali]
273+
if !ok {
274+
var fact goFixInlineAliasFact
275+
if pass.ImportObjectFact(ali, &fact) {
276+
inalias = &fact
277+
inlinableAliases[ali] = inalias
278+
}
279+
}
280+
if inalias == nil {
281+
continue // nope
282+
}
283+
curFile := currentFile(cur)
284+
285+
// We have an identifier A here (n), possibly qualified by a package identifier (sel.X,
286+
// where sel is the parent of X), // and an inlinable "type A = B" elsewhere (inali).
287+
// Consider replacing A with B.
288+
289+
// Check that the expression we are inlining (B) means the same thing
290+
// (refers to the same object) in n's scope as it does in A's scope.
291+
// If the RHS is not in the current package, AddImport will handle
292+
// shadowing, so we only need to worry about when both expressions
293+
// are in the current package.
294+
if pass.Pkg.Path() == inalias.RHSPkgPath {
295+
// fcon.rhsObj is the object referred to by B in the definition of A.
296+
scope := pass.TypesInfo.Scopes[curFile].Innermost(n.Pos()) // n's scope
297+
_, obj := scope.LookupParent(inalias.RHSName, n.Pos()) // what "B" means in n's scope
298+
if obj == nil {
299+
// Should be impossible: if code at n can refer to the LHS,
300+
// it can refer to the RHS.
301+
panic(fmt.Sprintf("no object for inlinable alias %s RHS %s", n.Name, inalias.RHSName))
302+
}
303+
if obj != inalias.rhsObj {
304+
// "B" means something different here than at the inlinable const's scope.
305+
continue
306+
}
307+
} else if !analysisinternal.CanImport(pass.Pkg.Path(), inalias.RHSPkgPath) {
308+
// If this package can't see the RHS's package, we can't inline.
309+
continue
310+
}
311+
var (
312+
importPrefix string
313+
edits []analysis.TextEdit
314+
)
315+
if inalias.RHSPkgPath != pass.Pkg.Path() {
316+
_, importPrefix, edits = analysisinternal.AddImport(
317+
pass.TypesInfo, curFile, inalias.RHSPkgName, inalias.RHSPkgPath, inalias.RHSName, n.Pos())
318+
}
319+
// If n is qualified by a package identifier, we'll need the full selector expression.
320+
var expr ast.Expr = n
321+
if e, _ := cur.Edge(); e == edge.SelectorExpr_Sel {
322+
expr = cur.Parent().Node().(ast.Expr)
323+
}
324+
reportInline(pass, "type alias", "Type alias", expr, edits, importPrefix+inalias.RHSName)
325+
continue
326+
}
221327
// If the identifier is a use of an inlinable constant, suggest inlining it.
222328
if con, ok := pass.TypesInfo.Uses[n].(*types.Const); ok {
223329
incon, ok := inlinableConsts[con]
@@ -233,14 +339,10 @@ func run(pass *analysis.Pass) (any, error) {
233339
}
234340

235341
// If n is qualified by a package identifier, we'll need the full selector expression.
236-
var sel *ast.SelectorExpr
237-
if e, _ := cur.Edge(); e == edge.SelectorExpr_Sel {
238-
sel = cur.Parent().Node().(*ast.SelectorExpr)
239-
}
240342
curFile := currentFile(cur)
241343

242-
// We have an identifier A here (n), possibly qualified by a package identifier (sel.X),
243-
// and an inlinable "const A = B" elsewhere (fcon).
344+
// We have an identifier A here (n), possibly qualified by a package identifier (sel.X,
345+
// where sel is the parent of n), // and an inlinable "const A = B" elsewhere (incon).
244346
// Consider replacing A with B.
245347

246348
// Check that the expression we are inlining (B) means the same thing
@@ -249,7 +351,7 @@ func run(pass *analysis.Pass) (any, error) {
249351
// shadowing, so we only need to worry about when both expressions
250352
// are in the current package.
251353
if pass.Pkg.Path() == incon.RHSPkgPath {
252-
// fcon.rhsObj is the object referred to by B in the definition of A.
354+
// incon.rhsObj is the object referred to by B in the definition of A.
253355
scope := pass.TypesInfo.Scopes[curFile].Innermost(n.Pos()) // n's scope
254356
_, obj := scope.LookupParent(incon.RHSName, n.Pos()) // what "B" means in n's scope
255357
if obj == nil {
@@ -273,38 +375,38 @@ func run(pass *analysis.Pass) (any, error) {
273375
_, importPrefix, edits = analysisinternal.AddImport(
274376
pass.TypesInfo, curFile, incon.RHSPkgName, incon.RHSPkgPath, incon.RHSName, n.Pos())
275377
}
276-
var (
277-
pos = n.Pos()
278-
end = n.End()
279-
name = n.Name
280-
)
281-
// Replace the entire SelectorExpr if there is one.
282-
if sel != nil {
283-
pos = sel.Pos()
284-
end = sel.End()
285-
name = sel.X.(*ast.Ident).Name + "." + n.Name
378+
// If n is qualified by a package identifier, we'll need the full selector expression.
379+
var expr ast.Expr = n
380+
if e, _ := cur.Edge(); e == edge.SelectorExpr_Sel {
381+
expr = cur.Parent().Node().(ast.Expr)
286382
}
287-
edits = append(edits, analysis.TextEdit{
288-
Pos: pos,
289-
End: end,
290-
NewText: []byte(importPrefix + incon.RHSName),
291-
})
292-
pass.Report(analysis.Diagnostic{
293-
Pos: pos,
294-
End: end,
295-
Message: fmt.Sprintf("Constant %s should be inlined", name),
296-
SuggestedFixes: []analysis.SuggestedFix{{
297-
Message: fmt.Sprintf("Inline constant %s", name),
298-
TextEdits: edits,
299-
}},
300-
})
383+
reportInline(pass, "constant", "Constant", expr, edits, importPrefix+incon.RHSName)
301384
}
302385
}
303386
}
304387

305388
return nil, nil
306389
}
307390

391+
// reportInline reports a diagnostic for fixing an inlinable name.
392+
func reportInline(pass *analysis.Pass, kind, capKind string, ident ast.Expr, edits []analysis.TextEdit, newText string) {
393+
edits = append(edits, analysis.TextEdit{
394+
Pos: ident.Pos(),
395+
End: ident.End(),
396+
NewText: []byte(newText),
397+
})
398+
name := analysisinternal.Format(pass.Fset, ident)
399+
pass.Report(analysis.Diagnostic{
400+
Pos: ident.Pos(),
401+
End: ident.End(),
402+
Message: fmt.Sprintf("%s %s should be inlined", capKind, name),
403+
SuggestedFixes: []analysis.SuggestedFix{{
404+
Message: fmt.Sprintf("Inline %s %s", kind, name),
405+
TextEdits: edits,
406+
}},
407+
})
408+
}
409+
308410
// hasFixInline reports the presence of a "//go:fix inline" directive
309411
// in the comments.
310412
func hasFixInline(cg *ast.CommentGroup) bool {
@@ -339,6 +441,22 @@ func (c *goFixInlineConstFact) String() string {
339441

340442
func (*goFixInlineConstFact) AFact() {}
341443

444+
// A goFixInlineAliasFact is exported for each type alias marked "//go:fix inline".
445+
// It holds information about an inlinable type alias. Gob-serializable.
446+
type goFixInlineAliasFact struct {
447+
// Information about "type LHSName = RHSName".
448+
RHSName string
449+
RHSPkgPath string
450+
RHSPkgName string
451+
rhsObj types.Object // for current package
452+
}
453+
454+
func (c *goFixInlineAliasFact) String() string {
455+
return fmt.Sprintf("goFixInline alias %q.%s", c.RHSPkgPath, c.RHSName)
456+
}
457+
458+
func (*goFixInlineAliasFact) AFact() {}
459+
342460
func discard(string, ...any) {}
343461

344462
var builtinIota = types.Universe.Lookup("iota")

gopls/internal/analysis/gofix/testdata/src/a/a.go

+15
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,18 @@ func shadow() {
101101

102102
_ = x
103103
}
104+
105+
// Type aliases
106+
107+
//go:fix inline
108+
type A = T // want A: `goFixInline alias "a".T`
109+
110+
var _ A // want `Type alias A should be inlined`
111+
112+
type B = []T // nope: only named RHSs
113+
114+
//go:fix inline
115+
type AA = // want AA: `goFixInline alias "a".A`
116+
A // want `Type alias A should be inlined`
117+
118+
var _ AA // want `Type alias AA should be inlined`

gopls/internal/analysis/gofix/testdata/src/a/a.go.golden

+15
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,18 @@ func shadow() {
101101

102102
_ = x
103103
}
104+
105+
// Type aliases
106+
107+
//go:fix inline
108+
type A = T // want A: `goFixInline alias "a".T`
109+
110+
var _ T // want `Type alias A should be inlined`
111+
112+
type B = []T // nope: only named RHSs
113+
114+
//go:fix inline
115+
type AA = // want AA: `goFixInline alias "a".A`
116+
T // want `Type alias A should be inlined`
117+
118+
var _ A // want `Type alias AA should be inlined`

gopls/internal/analysis/gofix/testdata/src/b/b.go

+2
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,5 @@ func g() {
3030
}
3131

3232
const d = a.D // nope: a.D refers to a constant in a package that is not visible here.
33+
34+
var _ a.A // want `Type alias a\.A should be inlined`

gopls/internal/analysis/gofix/testdata/src/b/b.go.golden

+2
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,5 @@ func g() {
3434
}
3535

3636
const d = a.D // nope: a.D refers to a constant in a package that is not visible here.
37+
38+
var _ a.T // want `Type alias a\.A should be inlined`

0 commit comments

Comments
 (0)