src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

qf1008.go (4566B)


      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) (any, 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 		for _, sel := range sels {
     89 		fieldLoop:
     90 			for base, fields := pass.TypesInfo.TypeOf(sel.X), sel.Fields; len(fields) >= 2; base, fields = pass.TypesInfo.ObjectOf(fields[0]).Type(), fields[1:] {
     91 				hop1 := fields[0]
     92 				hop2 := fields[1]
     93 
     94 				// the selector expression might be a qualified identifier, which cannot be simplified
     95 				if base == types.Typ[types.Invalid] {
     96 					continue fieldLoop
     97 				}
     98 
     99 				// Check if we can skip a field in the chain of selectors.
    100 				// We can skip a field 'b' if a.b.c and a.c resolve to the same object and take the same path.
    101 				//
    102 				// We set addressable to true unconditionally because we've already successfully type-checked the program,
    103 				// which means either the selector doesn't need addressability, or it is addressable.
    104 				leftObj, leftLeg, _ := types.LookupFieldOrMethod(base, true, pass.Pkg, hop1.Name)
    105 
    106 				// We can't skip fields that aren't embedded
    107 				if !leftObj.(*types.Var).Embedded() {
    108 					continue fieldLoop
    109 				}
    110 
    111 				directObj, directPath, _ := types.LookupFieldOrMethod(base, true, pass.Pkg, hop2.Name)
    112 
    113 				// Fail fast if omitting the embedded field leads to a different object
    114 				if directObj != pass.TypesInfo.ObjectOf(hop2) {
    115 					continue fieldLoop
    116 				}
    117 
    118 				_, rightLeg, _ := types.LookupFieldOrMethod(leftObj.Type(), true, pass.Pkg, hop2.Name)
    119 
    120 				// Fail fast if the paths are obviously different
    121 				if len(directPath) != len(leftLeg)+len(rightLeg) {
    122 					continue fieldLoop
    123 				}
    124 
    125 				// Make sure that omitting the embedded field will take the same path to the final object.
    126 				// Multiple paths involving different fields may lead to the same type-checker object, causing different runtime behavior.
    127 				for i := range directPath {
    128 					if i < len(leftLeg) {
    129 						if leftLeg[i] != directPath[i] {
    130 							continue fieldLoop
    131 						}
    132 					} else {
    133 						if rightLeg[i-len(leftLeg)] != directPath[i] {
    134 							continue fieldLoop
    135 						}
    136 					}
    137 				}
    138 
    139 				e := edit.Delete(edit.Range{hop1.Pos(), hop2.Pos()})
    140 				report.Report(pass, hop1, fmt.Sprintf("could remove embedded field %q from selector", hop1.Name),
    141 					report.Fixes(edit.Fix(fmt.Sprintf("Remove embedded field %q from selector", hop1.Name), e)))
    142 			}
    143 		}
    144 	}
    145 	code.Preorder(pass, fn, (*ast.SelectorExpr)(nil))
    146 	return nil, nil
    147 }