github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/quickfix/qf1008/qf1008.go (about) 1 package qf1008 2 3 import ( 4 "fmt" 5 "go/ast" 6 "go/types" 7 8 "github.com/amarpal/go-tools/analysis/code" 9 "github.com/amarpal/go-tools/analysis/edit" 10 "github.com/amarpal/go-tools/analysis/lint" 11 "github.com/amarpal/go-tools/analysis/report" 12 "github.com/amarpal/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.Documentation{ 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 }