1 package qf1008
2 3 import (
4 "fmt"
5 "go/ast"
6 "go/types"
7 8 "honnef.co/go/tools/analysis/code"
9 "honnef.co/go/tools/analysis/edit"
10 "honnef.co/go/tools/analysis/lint"
11 "honnef.co/go/tools/analysis/report"
12 "honnef.co/go/tools/go/ast/astutil"
13 14 "golang.org/x/tools/go/analysis"
15 "golang.org/x/tools/go/analysis/passes/inspect"
16 )
17 18 var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
19 Analyzer: &analysis.Analyzer{
20 Name: "QF1008",
21 Run: run,
22 Requires: []*analysis.Analyzer{inspect.Analyzer},
23 },
24 Doc: &lint.RawDocumentation{
25 Title: "Omit embedded fields from selector expression",
26 Since: "2021.1",
27 Severity: lint.SeverityHint,
28 },
29 })
30 31 var Analyzer = SCAnalyzer.Analyzer
32 33 func run(pass *analysis.Pass) (interface{}, error) {
34 type Selector struct {
35 Node *ast.SelectorExpr
36 X ast.Expr
37 Fields []*ast.Ident
38 }
39 40 // extractSelectors extracts uninterrupted sequences of selector expressions.
41 // For example, for a.b.c().d.e[0].f.g three sequences will be returned: (X=a, X.b.c), (X=a.b.c(), X.d.e), and (X=a.b.c().d.e[0], X.f.g)
42 //
43 // It returns nil if the provided selector expression is not the root of a set of sequences.
44 // For example, for a.b.c, if node is b.c, no selectors will be returned.
45 extractSelectors := func(expr *ast.SelectorExpr) []Selector {
46 path, _ := astutil.PathEnclosingInterval(code.File(pass, expr), expr.Pos(), expr.Pos())
47 for i := len(path) - 1; i >= 0; i-- {
48 if el, ok := path[i].(*ast.SelectorExpr); ok {
49 if el != expr {
50 // this expression is a subset of the entire chain, don't look at it.
51 return nil
52 }
53 break
54 }
55 }
56 57 inChain := false
58 var out []Selector
59 for _, el := range path {
60 if expr, ok := el.(*ast.SelectorExpr); ok {
61 if !inChain {
62 inChain = true
63 out = append(out, Selector{X: expr.X})
64 }
65 sel := &out[len(out)-1]
66 sel.Fields = append(sel.Fields, expr.Sel)
67 sel.Node = expr
68 } else if inChain {
69 inChain = false
70 }
71 }
72 return out
73 }
74 75 fn := func(node ast.Node) {
76 expr := node.(*ast.SelectorExpr)
77 78 if _, ok := expr.X.(*ast.SelectorExpr); !ok {
79 // Avoid the expensive call to PathEnclosingInterval for the common 1-level deep selector, which cannot be shortened.
80 return
81 }
82 83 sels := extractSelectors(expr)
84 if len(sels) == 0 {
85 return
86 }
87 88 var edits []analysis.TextEdit
89 for _, sel := range sels {
90 fieldLoop:
91 for base, fields := pass.TypesInfo.TypeOf(sel.X), sel.Fields; len(fields) >= 2; base, fields = pass.TypesInfo.ObjectOf(fields[0]).Type(), fields[1:] {
92 hop1 := fields[0]
93 hop2 := fields[1]
94 95 // the selector expression might be a qualified identifier, which cannot be simplified
96 if base == types.Typ[types.Invalid] {
97 continue fieldLoop
98 }
99 100 // Check if we can skip a field in the chain of selectors.
101 // We can skip a field 'b' if a.b.c and a.c resolve to the same object and take the same path.
102 //
103 // We set addressable to true unconditionally because we've already successfully type-checked the program,
104 // which means either the selector doesn't need addressability, or it is addressable.
105 leftObj, leftLeg, _ := types.LookupFieldOrMethod(base, true, pass.Pkg, hop1.Name)
106 107 // We can't skip fields that aren't embedded
108 if !leftObj.(*types.Var).Embedded() {
109 continue fieldLoop
110 }
111 112 directObj, directPath, _ := types.LookupFieldOrMethod(base, true, pass.Pkg, hop2.Name)
113 114 // Fail fast if omitting the embedded field leads to a different object
115 if directObj != pass.TypesInfo.ObjectOf(hop2) {
116 continue fieldLoop
117 }
118 119 _, rightLeg, _ := types.LookupFieldOrMethod(leftObj.Type(), true, pass.Pkg, hop2.Name)
120 121 // Fail fast if the paths are obviously different
122 if len(directPath) != len(leftLeg)+len(rightLeg) {
123 continue fieldLoop
124 }
125 126 // Make sure that omitting the embedded field will take the same path to the final object.
127 // Multiple paths involving different fields may lead to the same type-checker object, causing different runtime behavior.
128 for i := range directPath {
129 if i < len(leftLeg) {
130 if leftLeg[i] != directPath[i] {
131 continue fieldLoop
132 }
133 } else {
134 if rightLeg[i-len(leftLeg)] != directPath[i] {
135 continue fieldLoop
136 }
137 }
138 }
139 140 e := edit.Delete(edit.Range{hop1.Pos(), hop2.Pos()})
141 edits = append(edits, e)
142 report.Report(pass, hop1, fmt.Sprintf("could remove embedded field %q from selector", hop1.Name),
143 report.Fixes(edit.Fix(fmt.Sprintf("Remove embedded field %q from selector", hop1.Name), e)))
144 }
145 }
146 147 // Offer to simplify all selector expressions at once
148 if len(edits) > 1 {
149 // Hack to prevent gopls from applying the Unnecessary tag to the diagnostic. It applies the tag when all edits are deletions.
150 edits = append(edits, edit.ReplaceWithString(edit.Range{node.Pos(), node.Pos()}, ""))
151 report.Report(pass, node, "could simplify selectors", report.Fixes(edit.Fix("Remove all embedded fields from selector", edits...)))
152 }
153 }
154 code.Preorder(pass, fn, (*ast.SelectorExpr)(nil))
155 return nil, nil
156 }
157