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 }