github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/internal/sharedcheck/lint.go (about)

     1  package sharedcheck
     2  
     3  import (
     4  	"fmt"
     5  	"go/ast"
     6  	"go/token"
     7  	"go/types"
     8  
     9  	"github.com/amarpal/go-tools/analysis/code"
    10  	"github.com/amarpal/go-tools/analysis/edit"
    11  	"github.com/amarpal/go-tools/analysis/facts/generated"
    12  	"github.com/amarpal/go-tools/analysis/facts/tokenfile"
    13  	"github.com/amarpal/go-tools/analysis/report"
    14  	"github.com/amarpal/go-tools/go/ast/astutil"
    15  	"github.com/amarpal/go-tools/go/ir"
    16  	"github.com/amarpal/go-tools/go/ir/irutil"
    17  	"github.com/amarpal/go-tools/go/types/typeutil"
    18  	"github.com/amarpal/go-tools/internal/passes/buildir"
    19  
    20  	"golang.org/x/tools/go/analysis"
    21  	"golang.org/x/tools/go/analysis/passes/inspect"
    22  )
    23  
    24  func CheckRangeStringRunes(pass *analysis.Pass) (interface{}, error) {
    25  	for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
    26  		cb := func(node ast.Node) bool {
    27  			rng, ok := node.(*ast.RangeStmt)
    28  			if !ok || !astutil.IsBlank(rng.Key) {
    29  				return true
    30  			}
    31  
    32  			v, _ := fn.ValueForExpr(rng.X)
    33  
    34  			// Check that we're converting from string to []rune
    35  			val, _ := v.(*ir.Convert)
    36  			if val == nil {
    37  				return true
    38  			}
    39  			Tsrc, ok := typeutil.CoreType(val.X.Type()).(*types.Basic)
    40  			if !ok || Tsrc.Kind() != types.String {
    41  				return true
    42  			}
    43  			Tdst, ok := typeutil.CoreType(val.Type()).(*types.Slice)
    44  			if !ok {
    45  				return true
    46  			}
    47  			TdstElem, ok := Tdst.Elem().(*types.Basic)
    48  			if !ok || TdstElem.Kind() != types.Int32 {
    49  				return true
    50  			}
    51  
    52  			// Check that the result of the conversion is only used to
    53  			// range over
    54  			refs := val.Referrers()
    55  			if refs == nil {
    56  				return true
    57  			}
    58  
    59  			// Expect two refs: one for obtaining the length of the slice,
    60  			// one for accessing the elements
    61  			if len(irutil.FilterDebug(*refs)) != 2 {
    62  				// TODO(dh): right now, we check that only one place
    63  				// refers to our slice. This will miss cases such as
    64  				// ranging over the slice twice. Ideally, we'd ensure that
    65  				// the slice is only used for ranging over (without
    66  				// accessing the key), but that is harder to do because in
    67  				// IR form, ranging over a slice looks like an ordinary
    68  				// loop with index increments and slice accesses. We'd
    69  				// have to look at the associated AST node to check that
    70  				// it's a range statement.
    71  				return true
    72  			}
    73  
    74  			pass.Reportf(rng.Pos(), "should range over string, not []rune(string)")
    75  
    76  			return true
    77  		}
    78  		if source := fn.Source(); source != nil {
    79  			ast.Inspect(source, cb)
    80  		}
    81  	}
    82  	return nil, nil
    83  }
    84  
    85  // RedundantTypeInDeclarationChecker returns a checker that flags variable declarations with redundantly specified types.
    86  // That is, it flags 'var v T = e' where e's type is identical to T and 'var v = e' (or 'v := e') would have the same effect.
    87  //
    88  // It does not flag variables under the following conditions, to reduce the number of false positives:
    89  // - global variables – these often specify types to aid godoc
    90  // - files that use cgo – cgo code generation and pointer checking emits redundant types
    91  //
    92  // It does not flag variables under the following conditions, unless flagHelpfulTypes is true, to reduce the number of noisy positives:
    93  // - packages that import syscall or unsafe – these sometimes use this form of assignment to make sure types are as expected
    94  // - variables named the blank identifier – a pattern used to confirm the types of variables
    95  // - untyped expressions on the rhs – the explicitness might aid readability
    96  func RedundantTypeInDeclarationChecker(verb string, flagHelpfulTypes bool) *analysis.Analyzer {
    97  	fn := func(pass *analysis.Pass) (interface{}, error) {
    98  		eval := func(expr ast.Expr) (types.TypeAndValue, error) {
    99  			info := &types.Info{
   100  				Types: map[ast.Expr]types.TypeAndValue{},
   101  			}
   102  			err := types.CheckExpr(pass.Fset, pass.Pkg, expr.Pos(), expr, info)
   103  			return info.Types[expr], err
   104  		}
   105  
   106  		if !flagHelpfulTypes {
   107  			// Don't look at code in low-level packages
   108  			for _, imp := range pass.Pkg.Imports() {
   109  				if imp.Path() == "syscall" || imp.Path() == "unsafe" {
   110  					return nil, nil
   111  				}
   112  			}
   113  		}
   114  
   115  		fn := func(node ast.Node) {
   116  			decl := node.(*ast.GenDecl)
   117  			if decl.Tok != token.VAR {
   118  				return
   119  			}
   120  
   121  			gen, _ := code.Generator(pass, decl.Pos())
   122  			if gen == generated.Cgo {
   123  				// TODO(dh): remove this exception once we can use UsesCgo
   124  				return
   125  			}
   126  
   127  			// Delay looking up parent AST nodes until we have to
   128  			checkedDecl := false
   129  
   130  		specLoop:
   131  			for _, spec := range decl.Specs {
   132  				spec := spec.(*ast.ValueSpec)
   133  				if spec.Type == nil {
   134  					continue
   135  				}
   136  				if len(spec.Names) != len(spec.Values) {
   137  					continue
   138  				}
   139  				Tlhs := pass.TypesInfo.TypeOf(spec.Type)
   140  				for i, v := range spec.Values {
   141  					if !flagHelpfulTypes && spec.Names[i].Name == "_" {
   142  						continue specLoop
   143  					}
   144  					Trhs := pass.TypesInfo.TypeOf(v)
   145  					if !types.Identical(Tlhs, Trhs) {
   146  						continue specLoop
   147  					}
   148  
   149  					// Some expressions are untyped and get converted to the lhs type implicitly.
   150  					// This applies to untyped constants, shift operations with an untyped lhs, and possibly others.
   151  					//
   152  					// Check if the type is truly redundant, i.e. if the type on the lhs doesn't match the default type of the untyped constant.
   153  					tv, err := eval(v)
   154  					if err != nil {
   155  						panic(err)
   156  					}
   157  					if b, ok := tv.Type.(*types.Basic); ok && (b.Info()&types.IsUntyped) != 0 {
   158  						if Tlhs != types.Default(b) {
   159  							// The rhs is untyped and its default type differs from the explicit type on the lhs
   160  							continue specLoop
   161  						}
   162  						switch v := v.(type) {
   163  						case *ast.Ident:
   164  							// Only flag named constant rhs if it's a predeclared identifier.
   165  							// Don't flag other named constants, as the explicit type may aid readability.
   166  							if pass.TypesInfo.ObjectOf(v).Pkg() != nil && !flagHelpfulTypes {
   167  								continue specLoop
   168  							}
   169  						case *ast.BasicLit:
   170  							// Do flag basic literals
   171  						default:
   172  							// Don't flag untyped rhs expressions unless flagHelpfulTypes is set
   173  							if !flagHelpfulTypes {
   174  								continue specLoop
   175  							}
   176  						}
   177  					}
   178  				}
   179  
   180  				if !checkedDecl {
   181  					// Don't flag global variables. These often have explicit types for godoc's sake.
   182  					path, _ := astutil.PathEnclosingInterval(code.File(pass, decl), decl.Pos(), decl.Pos())
   183  				pathLoop:
   184  					for _, el := range path {
   185  						switch el.(type) {
   186  						case *ast.FuncDecl, *ast.FuncLit:
   187  							checkedDecl = true
   188  							break pathLoop
   189  						}
   190  					}
   191  					if !checkedDecl {
   192  						// decl is not inside a function
   193  						break specLoop
   194  					}
   195  				}
   196  
   197  				report.Report(pass, spec.Type, fmt.Sprintf("%s omit type %s from declaration; it will be inferred from the right-hand side", verb, report.Render(pass, spec.Type)), report.FilterGenerated(),
   198  					report.Fixes(edit.Fix("Remove redundant type", edit.Delete(spec.Type))))
   199  			}
   200  		}
   201  		code.Preorder(pass, fn, (*ast.GenDecl)(nil))
   202  		return nil, nil
   203  	}
   204  
   205  	return &analysis.Analyzer{
   206  		Run:      fn,
   207  		Requires: []*analysis.Analyzer{generated.Analyzer, inspect.Analyzer, tokenfile.Analyzer},
   208  	}
   209  }