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  }