// Package staticcheck contains a linter for Go source code. package staticcheck // import "honnef.co/go/tools/staticcheck" import ( "fmt" "go/ast" "go/constant" "go/token" "go/types" htmltemplate "html/template" "net/http" "regexp" "regexp/syntax" "sort" "strconv" "strings" "sync" texttemplate "text/template" "honnef.co/go/tools/deprecated" "honnef.co/go/tools/functions" "honnef.co/go/tools/internal/sharedcheck" "honnef.co/go/tools/lint" . "honnef.co/go/tools/lint/lintdsl" "honnef.co/go/tools/ssa" "honnef.co/go/tools/staticcheck/vrp" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/loader" ) func validRegexp(call *Call) { arg := call.Args[0] err := ValidateRegexp(arg.Value) if err != nil { arg.Invalid(err.Error()) } } type runeSlice []rune func (rs runeSlice) Len() int { return len(rs) } func (rs runeSlice) Less(i int, j int) bool { return rs[i] < rs[j] } func (rs runeSlice) Swap(i int, j int) { rs[i], rs[j] = rs[j], rs[i] } func utf8Cutset(call *Call) { arg := call.Args[1] if InvalidUTF8(arg.Value) { arg.Invalid(MsgInvalidUTF8) } } func uniqueCutset(call *Call) { arg := call.Args[1] if !UniqueStringCutset(arg.Value) { arg.Invalid(MsgNonUniqueCutset) } } func unmarshalPointer(name string, arg int) CallCheck { return func(call *Call) { if !Pointer(call.Args[arg].Value) { call.Args[arg].Invalid(fmt.Sprintf("%s expects to unmarshal into a pointer, but the provided value is not a pointer", name)) } } } func pointlessIntMath(call *Call) { if ConvertedFromInt(call.Args[0].Value) { call.Invalid(fmt.Sprintf("calling %s on a converted integer is pointless", CallName(call.Instr.Common()))) } } func checkValidHostPort(arg int) CallCheck { return func(call *Call) { if !ValidHostPort(call.Args[arg].Value) { call.Args[arg].Invalid(MsgInvalidHostPort) } } } var ( checkRegexpRules = map[string]CallCheck{ "regexp.MustCompile": validRegexp, "regexp.Compile": validRegexp, "regexp.Match": validRegexp, "regexp.MatchReader": validRegexp, "regexp.MatchString": validRegexp, } checkTimeParseRules = map[string]CallCheck{ "time.Parse": func(call *Call) { arg := call.Args[0] err := ValidateTimeLayout(arg.Value) if err != nil { arg.Invalid(err.Error()) } }, } checkEncodingBinaryRules = map[string]CallCheck{ "encoding/binary.Write": func(call *Call) { arg := call.Args[2] if !CanBinaryMarshal(call.Job, arg.Value) { arg.Invalid(fmt.Sprintf("value of type %s cannot be used with binary.Write", arg.Value.Value.Type())) } }, } checkURLsRules = map[string]CallCheck{ "net/url.Parse": func(call *Call) { arg := call.Args[0] err := ValidateURL(arg.Value) if err != nil { arg.Invalid(err.Error()) } }, } checkSyncPoolValueRules = map[string]CallCheck{ "(*sync.Pool).Put": func(call *Call) { arg := call.Args[0] typ := arg.Value.Value.Type() if !IsPointerLike(typ) { arg.Invalid("argument should be pointer-like to avoid allocations") } }, } checkRegexpFindAllRules = map[string]CallCheck{ "(*regexp.Regexp).FindAll": RepeatZeroTimes("a FindAll method", 1), "(*regexp.Regexp).FindAllIndex": RepeatZeroTimes("a FindAll method", 1), "(*regexp.Regexp).FindAllString": RepeatZeroTimes("a FindAll method", 1), "(*regexp.Regexp).FindAllStringIndex": RepeatZeroTimes("a FindAll method", 1), "(*regexp.Regexp).FindAllStringSubmatch": RepeatZeroTimes("a FindAll method", 1), "(*regexp.Regexp).FindAllStringSubmatchIndex": RepeatZeroTimes("a FindAll method", 1), "(*regexp.Regexp).FindAllSubmatch": RepeatZeroTimes("a FindAll method", 1), "(*regexp.Regexp).FindAllSubmatchIndex": RepeatZeroTimes("a FindAll method", 1), } checkUTF8CutsetRules = map[string]CallCheck{ "strings.IndexAny": utf8Cutset, "strings.LastIndexAny": utf8Cutset, "strings.ContainsAny": utf8Cutset, "strings.Trim": utf8Cutset, "strings.TrimLeft": utf8Cutset, "strings.TrimRight": utf8Cutset, } checkUniqueCutsetRules = map[string]CallCheck{ "strings.Trim": uniqueCutset, "strings.TrimLeft": uniqueCutset, "strings.TrimRight": uniqueCutset, } checkUnmarshalPointerRules = map[string]CallCheck{ "encoding/xml.Unmarshal": unmarshalPointer("xml.Unmarshal", 1), "(*encoding/xml.Decoder).Decode": unmarshalPointer("Decode", 0), "(*encoding/xml.Decoder).DecodeElement": unmarshalPointer("DecodeElement", 0), "encoding/json.Unmarshal": unmarshalPointer("json.Unmarshal", 1), "(*encoding/json.Decoder).Decode": unmarshalPointer("Decode", 0), } checkUnbufferedSignalChanRules = map[string]CallCheck{ "os/signal.Notify": func(call *Call) { arg := call.Args[0] if UnbufferedChannel(arg.Value) { arg.Invalid("the channel used with signal.Notify should be buffered") } }, } checkMathIntRules = map[string]CallCheck{ "math.Ceil": pointlessIntMath, "math.Floor": pointlessIntMath, "math.IsNaN": pointlessIntMath, "math.Trunc": pointlessIntMath, "math.IsInf": pointlessIntMath, } checkStringsReplaceZeroRules = map[string]CallCheck{ "strings.Replace": RepeatZeroTimes("strings.Replace", 3), "bytes.Replace": RepeatZeroTimes("bytes.Replace", 3), } checkListenAddressRules = map[string]CallCheck{ "net/http.ListenAndServe": checkValidHostPort(0), "net/http.ListenAndServeTLS": checkValidHostPort(0), } checkBytesEqualIPRules = map[string]CallCheck{ "bytes.Equal": func(call *Call) { if ConvertedFrom(call.Args[0].Value, "net.IP") && ConvertedFrom(call.Args[1].Value, "net.IP") { call.Invalid("use net.IP.Equal to compare net.IPs, not bytes.Equal") } }, } checkRegexpMatchLoopRules = map[string]CallCheck{ "regexp.Match": loopedRegexp("regexp.Match"), "regexp.MatchReader": loopedRegexp("regexp.MatchReader"), "regexp.MatchString": loopedRegexp("regexp.MatchString"), } ) type Checker struct { CheckGenerated bool funcDescs *functions.Descriptions deprecatedObjs map[types.Object]string } func NewChecker() *Checker { return &Checker{} } func (*Checker) Name() string { return "staticcheck" } func (*Checker) Prefix() string { return "SA" } func (c *Checker) Funcs() map[string]lint.Func { return map[string]lint.Func{ "SA1000": c.callChecker(checkRegexpRules), "SA1001": c.CheckTemplate, "SA1002": c.callChecker(checkTimeParseRules), "SA1003": c.callChecker(checkEncodingBinaryRules), "SA1004": c.CheckTimeSleepConstant, "SA1005": c.CheckExec, "SA1006": c.CheckUnsafePrintf, "SA1007": c.callChecker(checkURLsRules), "SA1008": c.CheckCanonicalHeaderKey, "SA1009": nil, "SA1010": c.callChecker(checkRegexpFindAllRules), "SA1011": c.callChecker(checkUTF8CutsetRules), "SA1012": c.CheckNilContext, "SA1013": c.CheckSeeker, "SA1014": c.callChecker(checkUnmarshalPointerRules), "SA1015": c.CheckLeakyTimeTick, "SA1016": c.CheckUntrappableSignal, "SA1017": c.callChecker(checkUnbufferedSignalChanRules), "SA1018": c.callChecker(checkStringsReplaceZeroRules), "SA1019": c.CheckDeprecated, "SA1020": c.callChecker(checkListenAddressRules), "SA1021": c.callChecker(checkBytesEqualIPRules), "SA1022": nil, "SA1023": c.CheckWriterBufferModified, "SA1024": c.callChecker(checkUniqueCutsetRules), "SA2000": c.CheckWaitgroupAdd, "SA2001": c.CheckEmptyCriticalSection, "SA2002": c.CheckConcurrentTesting, "SA2003": c.CheckDeferLock, "SA3000": c.CheckTestMainExit, "SA3001": c.CheckBenchmarkN, "SA4000": c.CheckLhsRhsIdentical, "SA4001": c.CheckIneffectiveCopy, "SA4002": c.CheckDiffSizeComparison, "SA4003": c.CheckUnsignedComparison, "SA4004": c.CheckIneffectiveLoop, "SA4005": nil, "SA4006": c.CheckUnreadVariableValues, // "SA4007": c.CheckPredeterminedBooleanExprs, "SA4007": nil, "SA4008": c.CheckLoopCondition, "SA4009": c.CheckArgOverwritten, "SA4010": c.CheckIneffectiveAppend, "SA4011": c.CheckScopedBreak, "SA4012": c.CheckNaNComparison, "SA4013": c.CheckDoubleNegation, "SA4014": c.CheckRepeatedIfElse, "SA4015": c.callChecker(checkMathIntRules), "SA4016": c.CheckSillyBitwiseOps, "SA4017": c.CheckPureFunctions, "SA4018": c.CheckSelfAssignment, "SA4019": c.CheckDuplicateBuildConstraints, "SA5000": c.CheckNilMaps, "SA5001": c.CheckEarlyDefer, "SA5002": c.CheckInfiniteEmptyLoop, "SA5003": c.CheckDeferInInfiniteLoop, "SA5004": c.CheckLoopEmptyDefault, "SA5005": c.CheckCyclicFinalizer, // "SA5006": c.CheckSliceOutOfBounds, "SA5007": c.CheckInfiniteRecursion, "SA6000": c.callChecker(checkRegexpMatchLoopRules), "SA6001": c.CheckMapBytesKey, "SA6002": c.callChecker(checkSyncPoolValueRules), "SA6003": c.CheckRangeStringRunes, "SA6004": c.CheckSillyRegexp, "SA9000": nil, "SA9001": c.CheckDubiousDeferInChannelRangeLoop, "SA9002": c.CheckNonOctalFileMode, "SA9003": c.CheckEmptyBranch, "SA9004": c.CheckMissingEnumTypesInDeclaration, } } func (c *Checker) filterGenerated(files []*ast.File) []*ast.File { if c.CheckGenerated { return files } var out []*ast.File for _, f := range files { if !IsGenerated(f) { out = append(out, f) } } return out } func (c *Checker) findDeprecated(prog *lint.Program) { var docs []*ast.CommentGroup var names []*ast.Ident doDocs := func(pkginfo *loader.PackageInfo, names []*ast.Ident, docs []*ast.CommentGroup) { var alt string for _, doc := range docs { if doc == nil { continue } parts := strings.Split(doc.Text(), "\n\n") last := parts[len(parts)-1] if !strings.HasPrefix(last, "Deprecated: ") { continue } alt = last[len("Deprecated: "):] alt = strings.Replace(alt, "\n", " ", -1) break } if alt == "" { return } for _, name := range names { obj := pkginfo.ObjectOf(name) c.deprecatedObjs[obj] = alt } } for _, pkginfo := range prog.Prog.AllPackages { for _, f := range pkginfo.Files { fn := func(node ast.Node) bool { if node == nil { return true } var ret bool switch node := node.(type) { case *ast.GenDecl: switch node.Tok { case token.TYPE, token.CONST, token.VAR: docs = append(docs, node.Doc) return true default: return false } case *ast.FuncDecl: docs = append(docs, node.Doc) names = []*ast.Ident{node.Name} ret = false case *ast.TypeSpec: docs = append(docs, node.Doc) names = []*ast.Ident{node.Name} ret = true case *ast.ValueSpec: docs = append(docs, node.Doc) names = node.Names ret = false case *ast.File: return true case *ast.StructType: for _, field := range node.Fields.List { doDocs(pkginfo, field.Names, []*ast.CommentGroup{field.Doc}) } return false case *ast.InterfaceType: for _, field := range node.Methods.List { doDocs(pkginfo, field.Names, []*ast.CommentGroup{field.Doc}) } return false default: return false } if len(names) == 0 || len(docs) == 0 { return ret } doDocs(pkginfo, names, docs) docs = docs[:0] names = nil return ret } ast.Inspect(f, fn) } } } func (c *Checker) Init(prog *lint.Program) { wg := &sync.WaitGroup{} wg.Add(2) go func() { c.funcDescs = functions.NewDescriptions(prog.SSA) for _, fn := range prog.AllFunctions { if fn.Blocks != nil { applyStdlibKnowledge(fn) ssa.OptimizeBlocks(fn) } } wg.Done() }() go func() { c.deprecatedObjs = map[types.Object]string{} c.findDeprecated(prog) wg.Done() }() wg.Wait() } func (c *Checker) isInLoop(b *ssa.BasicBlock) bool { sets := c.funcDescs.Get(b.Parent()).Loops for _, set := range sets { if set[b] { return true } } return false } func applyStdlibKnowledge(fn *ssa.Function) { if len(fn.Blocks) == 0 { return } // comma-ok receiving from a time.Tick channel will never return // ok == false, so any branching on the value of ok can be // replaced with an unconditional jump. This will primarily match // `for range time.Tick(x)` loops, but it can also match // user-written code. for _, block := range fn.Blocks { if len(block.Instrs) < 3 { continue } if len(block.Succs) != 2 { continue } var instrs []*ssa.Instruction for i, ins := range block.Instrs { if _, ok := ins.(*ssa.DebugRef); ok { continue } instrs = append(instrs, &block.Instrs[i]) } for i, ins := range instrs { unop, ok := (*ins).(*ssa.UnOp) if !ok || unop.Op != token.ARROW { continue } call, ok := unop.X.(*ssa.Call) if !ok { continue } if !IsCallTo(call.Common(), "time.Tick") { continue } ex, ok := (*instrs[i+1]).(*ssa.Extract) if !ok || ex.Tuple != unop || ex.Index != 1 { continue } ifstmt, ok := (*instrs[i+2]).(*ssa.If) if !ok || ifstmt.Cond != ex { continue } *instrs[i+2] = ssa.NewJump(block) succ := block.Succs[1] block.Succs = block.Succs[0:1] succ.RemovePred(block) } } } func hasType(j *lint.Job, expr ast.Expr, name string) bool { T := TypeOf(j, expr) return IsType(T, name) } func (c *Checker) CheckUntrappableSignal(j *lint.Job) { fn := func(node ast.Node) bool { call, ok := node.(*ast.CallExpr) if !ok { return true } if !IsCallToAnyAST(j, call, "os/signal.Ignore", "os/signal.Notify", "os/signal.Reset") { return true } for _, arg := range call.Args { if conv, ok := arg.(*ast.CallExpr); ok && isName(j, conv.Fun, "os.Signal") { arg = conv.Args[0] } if isName(j, arg, "os.Kill") || isName(j, arg, "syscall.SIGKILL") { j.Errorf(arg, "%s cannot be trapped (did you mean syscall.SIGTERM?)", Render(j, arg)) } if isName(j, arg, "syscall.SIGSTOP") { j.Errorf(arg, "%s signal cannot be trapped", Render(j, arg)) } } return true } for _, f := range j.Program.Files { ast.Inspect(f, fn) } } func (c *Checker) CheckTemplate(j *lint.Job) { fn := func(node ast.Node) bool { call, ok := node.(*ast.CallExpr) if !ok { return true } var kind string if IsCallToAST(j, call, "(*text/template.Template).Parse") { kind = "text" } else if IsCallToAST(j, call, "(*html/template.Template).Parse") { kind = "html" } else { return true } sel := call.Fun.(*ast.SelectorExpr) if !IsCallToAST(j, sel.X, "text/template.New") && !IsCallToAST(j, sel.X, "html/template.New") { // TODO(dh): this is a cheap workaround for templates with // different delims. A better solution with less false // negatives would use data flow analysis to see where the // template comes from and where it has been return true } s, ok := ExprToString(j, call.Args[0]) if !ok { return true } var err error switch kind { case "text": _, err = texttemplate.New("").Parse(s) case "html": _, err = htmltemplate.New("").Parse(s) } if err != nil { // TODO(dominikh): whitelist other parse errors, if any if strings.Contains(err.Error(), "unexpected") { j.Errorf(call.Args[0], "%s", err) } } return true } for _, f := range j.Program.Files { ast.Inspect(f, fn) } } func (c *Checker) CheckTimeSleepConstant(j *lint.Job) { fn := func(node ast.Node) bool { call, ok := node.(*ast.CallExpr) if !ok { return true } if !IsCallToAST(j, call, "time.Sleep") { return true } lit, ok := call.Args[0].(*ast.BasicLit) if !ok { return true } n, err := strconv.Atoi(lit.Value) if err != nil { return true } if n == 0 || n > 120 { // time.Sleep(0) is a seldom used pattern in concurrency // tests. >120 might be intentional. 120 was chosen // because the user could've meant 2 minutes. return true } recommendation := "time.Sleep(time.Nanosecond)" if n != 1 { recommendation = fmt.Sprintf("time.Sleep(%d * time.Nanosecond)", n) } j.Errorf(call.Args[0], "sleeping for %d nanoseconds is probably a bug. Be explicit if it isn't: %s", n, recommendation) return true } for _, f := range j.Program.Files { ast.Inspect(f, fn) } } func (c *Checker) CheckWaitgroupAdd(j *lint.Job) { fn := func(node ast.Node) bool { g, ok := node.(*ast.GoStmt) if !ok { return true } fun, ok := g.Call.Fun.(*ast.FuncLit) if !ok { return true } if len(fun.Body.List) == 0 { return true } stmt, ok := fun.Body.List[0].(*ast.ExprStmt) if !ok { return true } call, ok := stmt.X.(*ast.CallExpr) if !ok { return true } sel, ok := call.Fun.(*ast.SelectorExpr) if !ok { return true } fn, ok := ObjectOf(j, sel.Sel).(*types.Func) if !ok { return true } if fn.FullName() == "(*sync.WaitGroup).Add" { j.Errorf(sel, "should call %s before starting the goroutine to avoid a race", Render(j, stmt)) } return true } for _, f := range j.Program.Files { ast.Inspect(f, fn) } } func (c *Checker) CheckInfiniteEmptyLoop(j *lint.Job) { fn := func(node ast.Node) bool { loop, ok := node.(*ast.ForStmt) if !ok || len(loop.Body.List) != 0 || loop.Post != nil { return true } if loop.Init != nil { // TODO(dh): this isn't strictly necessary, it just makes // the check easier. return true } // An empty loop is bad news in two cases: 1) The loop has no // condition. In that case, it's just a loop that spins // forever and as fast as it can, keeping a core busy. 2) The // loop condition only consists of variable or field reads and // operators on those. The only way those could change their // value is with unsynchronised access, which constitutes a // data race. // // If the condition contains any function calls, its behaviour // is dynamic and the loop might terminate. Similarly for // channel receives. if loop.Cond != nil && hasSideEffects(loop.Cond) { return true } j.Errorf(loop, "this loop will spin, using 100%% CPU") if loop.Cond != nil { j.Errorf(loop, "loop condition never changes or has a race condition") } return true } for _, f := range j.Program.Files { ast.Inspect(f, fn) } } func (c *Checker) CheckDeferInInfiniteLoop(j *lint.Job) { fn := func(node ast.Node) bool { mightExit := false var defers []ast.Stmt loop, ok := node.(*ast.ForStmt) if !ok || loop.Cond != nil { return true } fn2 := func(node ast.Node) bool { switch stmt := node.(type) { case *ast.ReturnStmt: mightExit = true case *ast.BranchStmt: // TODO(dominikh): if this sees a break in a switch or // select, it doesn't check if it breaks the loop or // just the select/switch. This causes some false // negatives. if stmt.Tok == token.BREAK { mightExit = true } case *ast.DeferStmt: defers = append(defers, stmt) case *ast.FuncLit: // Don't look into function bodies return false } return true } ast.Inspect(loop.Body, fn2) if mightExit { return true } for _, stmt := range defers { j.Errorf(stmt, "defers in this infinite loop will never run") } return true } for _, f := range j.Program.Files { ast.Inspect(f, fn) } } func (c *Checker) CheckDubiousDeferInChannelRangeLoop(j *lint.Job) { fn := func(node ast.Node) bool { loop, ok := node.(*ast.RangeStmt) if !ok { return true } typ := TypeOf(j, loop.X) _, ok = typ.Underlying().(*types.Chan) if !ok { return true } fn2 := func(node ast.Node) bool { switch stmt := node.(type) { case *ast.DeferStmt: j.Errorf(stmt, "defers in this range loop won't run unless the channel gets closed") case *ast.FuncLit: // Don't look into function bodies return false } return true } ast.Inspect(loop.Body, fn2) return true } for _, f := range j.Program.Files { ast.Inspect(f, fn) } } func (c *Checker) CheckTestMainExit(j *lint.Job) { fn := func(node ast.Node) bool { if !isTestMain(j, node) { return true } arg := ObjectOf(j, node.(*ast.FuncDecl).Type.Params.List[0].Names[0]) callsRun := false fn2 := func(node ast.Node) bool { call, ok := node.(*ast.CallExpr) if !ok { return true } sel, ok := call.Fun.(*ast.SelectorExpr) if !ok { return true } ident, ok := sel.X.(*ast.Ident) if !ok { return true } if arg != ObjectOf(j, ident) { return true } if sel.Sel.Name == "Run" { callsRun = true return false } return true } ast.Inspect(node.(*ast.FuncDecl).Body, fn2) callsExit := false fn3 := func(node ast.Node) bool { if IsCallToAST(j, node, "os.Exit") { callsExit = true return false } return true } ast.Inspect(node.(*ast.FuncDecl).Body, fn3) if !callsExit && callsRun { j.Errorf(node, "TestMain should call os.Exit to set exit code") } return true } for _, f := range j.Program.Files { ast.Inspect(f, fn) } } func isTestMain(j *lint.Job, node ast.Node) bool { decl, ok := node.(*ast.FuncDecl) if !ok { return false } if decl.Name.Name != "TestMain" { return false } if len(decl.Type.Params.List) != 1 { return false } arg := decl.Type.Params.List[0] if len(arg.Names) != 1 { return false } return IsOfType(j, arg.Type, "*testing.M") } func (c *Checker) CheckExec(j *lint.Job) { fn := func(node ast.Node) bool { call, ok := node.(*ast.CallExpr) if !ok { return true } if !IsCallToAST(j, call, "os/exec.Command") { return true } val, ok := ExprToString(j, call.Args[0]) if !ok { return true } if !strings.Contains(val, " ") || strings.Contains(val, `\`) || strings.Contains(val, "/") { return true } j.Errorf(call.Args[0], "first argument to exec.Command looks like a shell command, but a program name or path are expected") return true } for _, f := range j.Program.Files { ast.Inspect(f, fn) } } func (c *Checker) CheckLoopEmptyDefault(j *lint.Job) { fn := func(node ast.Node) bool { loop, ok := node.(*ast.ForStmt) if !ok || len(loop.Body.List) != 1 || loop.Cond != nil || loop.Init != nil { return true } sel, ok := loop.Body.List[0].(*ast.SelectStmt) if !ok { return true } for _, c := range sel.Body.List { if comm, ok := c.(*ast.CommClause); ok && comm.Comm == nil && len(comm.Body) == 0 { j.Errorf(comm, "should not have an empty default case in a for+select loop. The loop will spin.") } } return true } for _, f := range j.Program.Files { ast.Inspect(f, fn) } } func (c *Checker) CheckLhsRhsIdentical(j *lint.Job) { fn := func(node ast.Node) bool { op, ok := node.(*ast.BinaryExpr) if !ok { return true } switch op.Op { case token.EQL, token.NEQ: if basic, ok := TypeOf(j, op.X).(*types.Basic); ok { if kind := basic.Kind(); kind == types.Float32 || kind == types.Float64 { // f == f and f != f might be used to check for NaN return true } } case token.SUB, token.QUO, token.AND, token.REM, token.OR, token.XOR, token.AND_NOT, token.LAND, token.LOR, token.LSS, token.GTR, token.LEQ, token.GEQ: default: // For some ops, such as + and *, it can make sense to // have identical operands return true } if Render(j, op.X) != Render(j, op.Y) { return true } j.Errorf(op, "identical expressions on the left and right side of the '%s' operator", op.Op) return true } for _, f := range j.Program.Files { ast.Inspect(f, fn) } } func (c *Checker) CheckScopedBreak(j *lint.Job) { fn := func(node ast.Node) bool { var body *ast.BlockStmt switch node := node.(type) { case *ast.ForStmt: body = node.Body case *ast.RangeStmt: body = node.Body default: return true } for _, stmt := range body.List { var blocks [][]ast.Stmt switch stmt := stmt.(type) { case *ast.SwitchStmt: for _, c := range stmt.Body.List { blocks = append(blocks, c.(*ast.CaseClause).Body) } case *ast.SelectStmt: for _, c := range stmt.Body.List { blocks = append(blocks, c.(*ast.CommClause).Body) } default: continue } for _, body := range blocks { if len(body) == 0 { continue } lasts := []ast.Stmt{body[len(body)-1]} // TODO(dh): unfold all levels of nested block // statements, not just a single level if statement if ifs, ok := lasts[0].(*ast.IfStmt); ok { if len(ifs.Body.List) == 0 { continue } lasts[0] = ifs.Body.List[len(ifs.Body.List)-1] if block, ok := ifs.Else.(*ast.BlockStmt); ok { if len(block.List) != 0 { lasts = append(lasts, block.List[len(block.List)-1]) } } } for _, last := range lasts { branch, ok := last.(*ast.BranchStmt) if !ok || branch.Tok != token.BREAK || branch.Label != nil { continue } j.Errorf(branch, "ineffective break statement. Did you mean to break out of the outer loop?") } } } return true } for _, f := range j.Program.Files { ast.Inspect(f, fn) } } func (c *Checker) CheckUnsafePrintf(j *lint.Job) { fn := func(node ast.Node) bool { call, ok := node.(*ast.CallExpr) if !ok { return true } if !IsCallToAnyAST(j, call, "fmt.Printf", "fmt.Sprintf", "log.Printf") { return true } if len(call.Args) != 1 { return true } switch call.Args[0].(type) { case *ast.CallExpr, *ast.Ident: default: return true } j.Errorf(call.Args[0], "printf-style function with dynamic first argument and no further arguments should use print-style function instead") return true } for _, f := range j.Program.Files { ast.Inspect(f, fn) } } func (c *Checker) CheckEarlyDefer(j *lint.Job) { fn := func(node ast.Node) bool { block, ok := node.(*ast.BlockStmt) if !ok { return true } if len(block.List) < 2 { return true } for i, stmt := range block.List { if i == len(block.List)-1 { break } assign, ok := stmt.(*ast.AssignStmt) if !ok { continue } if len(assign.Rhs) != 1 { continue } if len(assign.Lhs) < 2 { continue } if lhs, ok := assign.Lhs[len(assign.Lhs)-1].(*ast.Ident); ok && lhs.Name == "_" { continue } call, ok := assign.Rhs[0].(*ast.CallExpr) if !ok { continue } sig, ok := TypeOf(j, call.Fun).(*types.Signature) if !ok { continue } if sig.Results().Len() < 2 { continue } last := sig.Results().At(sig.Results().Len() - 1) // FIXME(dh): check that it's error from universe, not // another type of the same name if last.Type().String() != "error" { continue } lhs, ok := assign.Lhs[0].(*ast.Ident) if !ok { continue } def, ok := block.List[i+1].(*ast.DeferStmt) if !ok { continue } sel, ok := def.Call.Fun.(*ast.SelectorExpr) if !ok { continue } ident, ok := selectorX(sel).(*ast.Ident) if !ok { continue } if ident.Obj != lhs.Obj { continue } if sel.Sel.Name != "Close" { continue } j.Errorf(def, "should check returned error before deferring %s", Render(j, def.Call)) } return true } for _, f := range j.Program.Files { ast.Inspect(f, fn) } } func selectorX(sel *ast.SelectorExpr) ast.Node { switch x := sel.X.(type) { case *ast.SelectorExpr: return selectorX(x) default: return x } } func (c *Checker) CheckEmptyCriticalSection(j *lint.Job) { // Initially it might seem like this check would be easier to // implement in SSA. After all, we're only checking for two // consecutive method calls. In reality, however, there may be any // number of other instructions between the lock and unlock, while // still constituting an empty critical section. For example, // given `m.x().Lock(); m.x().Unlock()`, there will be a call to // x(). In the AST-based approach, this has a tiny potential for a // false positive (the second call to x might be doing work that // is protected by the mutex). In an SSA-based approach, however, // it would miss a lot of real bugs. mutexParams := func(s ast.Stmt) (x ast.Expr, funcName string, ok bool) { expr, ok := s.(*ast.ExprStmt) if !ok { return nil, "", false } call, ok := expr.X.(*ast.CallExpr) if !ok { return nil, "", false } sel, ok := call.Fun.(*ast.SelectorExpr) if !ok { return nil, "", false } fn, ok := ObjectOf(j, sel.Sel).(*types.Func) if !ok { return nil, "", false } sig := fn.Type().(*types.Signature) if sig.Params().Len() != 0 || sig.Results().Len() != 0 { return nil, "", false } return sel.X, fn.Name(), true } fn := func(node ast.Node) bool { block, ok := node.(*ast.BlockStmt) if !ok { return true } if len(block.List) < 2 { return true } for i := range block.List[:len(block.List)-1] { sel1, method1, ok1 := mutexParams(block.List[i]) sel2, method2, ok2 := mutexParams(block.List[i+1]) if !ok1 || !ok2 || Render(j, sel1) != Render(j, sel2) { continue } if (method1 == "Lock" && method2 == "Unlock") || (method1 == "RLock" && method2 == "RUnlock") { j.Errorf(block.List[i+1], "empty critical section") } } return true } for _, f := range j.Program.Files { ast.Inspect(f, fn) } } // cgo produces code like fn(&*_Cvar_kSomeCallbacks) which we don't // want to flag. var cgoIdent = regexp.MustCompile(`^_C(func|var)_.+$`) func (c *Checker) CheckIneffectiveCopy(j *lint.Job) { fn := func(node ast.Node) bool { if unary, ok := node.(*ast.UnaryExpr); ok { if star, ok := unary.X.(*ast.StarExpr); ok && unary.Op == token.AND { ident, ok := star.X.(*ast.Ident) if !ok || !cgoIdent.MatchString(ident.Name) { j.Errorf(unary, "&*x will be simplified to x. It will not copy x.") } } } if star, ok := node.(*ast.StarExpr); ok { if unary, ok := star.X.(*ast.UnaryExpr); ok && unary.Op == token.AND { j.Errorf(star, "*&x will be simplified to x. It will not copy x.") } } return true } for _, f := range j.Program.Files { ast.Inspect(f, fn) } } func (c *Checker) CheckDiffSizeComparison(j *lint.Job) { for _, ssafn := range j.Program.InitialFunctions { for _, b := range ssafn.Blocks { for _, ins := range b.Instrs { binop, ok := ins.(*ssa.BinOp) if !ok { continue } if binop.Op != token.EQL && binop.Op != token.NEQ { continue } _, ok1 := binop.X.(*ssa.Slice) _, ok2 := binop.Y.(*ssa.Slice) if !ok1 && !ok2 { continue } r := c.funcDescs.Get(ssafn).Ranges r1, ok1 := r.Get(binop.X).(vrp.StringInterval) r2, ok2 := r.Get(binop.Y).(vrp.StringInterval) if !ok1 || !ok2 { continue } if r1.Length.Intersection(r2.Length).Empty() { j.Errorf(binop, "comparing strings of different sizes for equality will always return false") } } } } } func (c *Checker) CheckCanonicalHeaderKey(j *lint.Job) { fn := func(node ast.Node) bool { assign, ok := node.(*ast.AssignStmt) if ok { // TODO(dh): This risks missing some Header reads, for // example in `h1["foo"] = h2["foo"]` – these edge // cases are probably rare enough to ignore for now. for _, expr := range assign.Lhs { op, ok := expr.(*ast.IndexExpr) if !ok { continue } if hasType(j, op.X, "net/http.Header") { return false } } return true } op, ok := node.(*ast.IndexExpr) if !ok { return true } if !hasType(j, op.X, "net/http.Header") { return true } s, ok := ExprToString(j, op.Index) if !ok { return true } if s == http.CanonicalHeaderKey(s) { return true } j.Errorf(op, "keys in http.Header are canonicalized, %q is not canonical; fix the constant or use http.CanonicalHeaderKey", s) return true } for _, f := range j.Program.Files { ast.Inspect(f, fn) } } func (c *Checker) CheckBenchmarkN(j *lint.Job) { fn := func(node ast.Node) bool { assign, ok := node.(*ast.AssignStmt) if !ok { return true } if len(assign.Lhs) != 1 || len(assign.Rhs) != 1 { return true } sel, ok := assign.Lhs[0].(*ast.SelectorExpr) if !ok { return true } if sel.Sel.Name != "N" { return true } if !hasType(j, sel.X, "*testing.B") { return true } j.Errorf(assign, "should not assign to %s", Render(j, sel)) return true } for _, f := range j.Program.Files { ast.Inspect(f, fn) } } func (c *Checker) CheckUnreadVariableValues(j *lint.Job) { for _, ssafn := range j.Program.InitialFunctions { if IsExample(ssafn) { continue } node := ssafn.Syntax() if node == nil { continue } ast.Inspect(node, func(node ast.Node) bool { assign, ok := node.(*ast.AssignStmt) if !ok { return true } if len(assign.Lhs) > 1 && len(assign.Rhs) == 1 { // Either a function call with multiple return values, // or a comma-ok assignment val, _ := ssafn.ValueForExpr(assign.Rhs[0]) if val == nil { return true } refs := val.Referrers() if refs == nil { return true } for _, ref := range *refs { ex, ok := ref.(*ssa.Extract) if !ok { continue } exrefs := ex.Referrers() if exrefs == nil { continue } if len(FilterDebug(*exrefs)) == 0 { lhs := assign.Lhs[ex.Index] if ident, ok := lhs.(*ast.Ident); !ok || ok && ident.Name == "_" { continue } j.Errorf(lhs, "this value of %s is never used", lhs) } } return true } for i, lhs := range assign.Lhs { rhs := assign.Rhs[i] if ident, ok := lhs.(*ast.Ident); !ok || ok && ident.Name == "_" { continue } val, _ := ssafn.ValueForExpr(rhs) if val == nil { continue } refs := val.Referrers() if refs == nil { // TODO investigate why refs can be nil return true } if len(FilterDebug(*refs)) == 0 { j.Errorf(lhs, "this value of %s is never used", lhs) } } return true }) } } func (c *Checker) CheckPredeterminedBooleanExprs(j *lint.Job) { for _, ssafn := range j.Program.InitialFunctions { for _, block := range ssafn.Blocks { for _, ins := range block.Instrs { ssabinop, ok := ins.(*ssa.BinOp) if !ok { continue } switch ssabinop.Op { case token.GTR, token.LSS, token.EQL, token.NEQ, token.LEQ, token.GEQ: default: continue } xs, ok1 := consts(ssabinop.X, nil, nil) ys, ok2 := consts(ssabinop.Y, nil, nil) if !ok1 || !ok2 || len(xs) == 0 || len(ys) == 0 { continue } trues := 0 for _, x := range xs { for _, y := range ys { if x.Value == nil { if y.Value == nil { trues++ } continue } if constant.Compare(x.Value, ssabinop.Op, y.Value) { trues++ } } } b := trues != 0 if trues == 0 || trues == len(xs)*len(ys) { j.Errorf(ssabinop, "binary expression is always %t for all possible values (%s %s %s)", b, xs, ssabinop.Op, ys) } } } } } func (c *Checker) CheckNilMaps(j *lint.Job) { for _, ssafn := range j.Program.InitialFunctions { for _, block := range ssafn.Blocks { for _, ins := range block.Instrs { mu, ok := ins.(*ssa.MapUpdate) if !ok { continue } c, ok := mu.Map.(*ssa.Const) if !ok { continue } if c.Value != nil { continue } j.Errorf(mu, "assignment to nil map") } } } } func (c *Checker) CheckUnsignedComparison(j *lint.Job) { fn := func(node ast.Node) bool { expr, ok := node.(*ast.BinaryExpr) if !ok { return true } tx := TypeOf(j, expr.X) basic, ok := tx.Underlying().(*types.Basic) if !ok { return true } if (basic.Info() & types.IsUnsigned) == 0 { return true } lit, ok := expr.Y.(*ast.BasicLit) if !ok || lit.Value != "0" { return true } switch expr.Op { case token.GEQ: j.Errorf(expr, "unsigned values are always >= 0") case token.LSS: j.Errorf(expr, "unsigned values are never < 0") } return true } for _, f := range j.Program.Files { ast.Inspect(f, fn) } } func consts(val ssa.Value, out []*ssa.Const, visitedPhis map[string]bool) ([]*ssa.Const, bool) { if visitedPhis == nil { visitedPhis = map[string]bool{} } var ok bool switch val := val.(type) { case *ssa.Phi: if visitedPhis[val.Name()] { break } visitedPhis[val.Name()] = true vals := val.Operands(nil) for _, phival := range vals { out, ok = consts(*phival, out, visitedPhis) if !ok { return nil, false } } case *ssa.Const: out = append(out, val) case *ssa.Convert: out, ok = consts(val.X, out, visitedPhis) if !ok { return nil, false } default: return nil, false } if len(out) < 2 { return out, true } uniq := []*ssa.Const{out[0]} for _, val := range out[1:] { if val.Value == uniq[len(uniq)-1].Value { continue } uniq = append(uniq, val) } return uniq, true } func (c *Checker) CheckLoopCondition(j *lint.Job) { for _, ssafn := range j.Program.InitialFunctions { fn := func(node ast.Node) bool { loop, ok := node.(*ast.ForStmt) if !ok { return true } if loop.Init == nil || loop.Cond == nil || loop.Post == nil { return true } init, ok := loop.Init.(*ast.AssignStmt) if !ok || len(init.Lhs) != 1 || len(init.Rhs) != 1 { return true } cond, ok := loop.Cond.(*ast.BinaryExpr) if !ok { return true } x, ok := cond.X.(*ast.Ident) if !ok { return true } lhs, ok := init.Lhs[0].(*ast.Ident) if !ok { return true } if x.Obj != lhs.Obj { return true } if _, ok := loop.Post.(*ast.IncDecStmt); !ok { return true } v, isAddr := ssafn.ValueForExpr(cond.X) if v == nil || isAddr { return true } switch v := v.(type) { case *ssa.Phi: ops := v.Operands(nil) if len(ops) != 2 { return true } _, ok := (*ops[0]).(*ssa.Const) if !ok { return true } sigma, ok := (*ops[1]).(*ssa.Sigma) if !ok { return true } if sigma.X != v { return true } case *ssa.UnOp: return true } j.Errorf(cond, "variable in loop condition never changes") return true } Inspect(ssafn.Syntax(), fn) } } func (c *Checker) CheckArgOverwritten(j *lint.Job) { for _, ssafn := range j.Program.InitialFunctions { fn := func(node ast.Node) bool { var typ *ast.FuncType var body *ast.BlockStmt switch fn := node.(type) { case *ast.FuncDecl: typ = fn.Type body = fn.Body case *ast.FuncLit: typ = fn.Type body = fn.Body } if body == nil { return true } if len(typ.Params.List) == 0 { return true } for _, field := range typ.Params.List { for _, arg := range field.Names { obj := ObjectOf(j, arg) var ssaobj *ssa.Parameter for _, param := range ssafn.Params { if param.Object() == obj { ssaobj = param break } } if ssaobj == nil { continue } refs := ssaobj.Referrers() if refs == nil { continue } if len(FilterDebug(*refs)) != 0 { continue } assigned := false ast.Inspect(body, func(node ast.Node) bool { assign, ok := node.(*ast.AssignStmt) if !ok { return true } for _, lhs := range assign.Lhs { ident, ok := lhs.(*ast.Ident) if !ok { continue } if ObjectOf(j, ident) == obj { assigned = true return false } } return true }) if assigned { j.Errorf(arg, "argument %s is overwritten before first use", arg) } } } return true } Inspect(ssafn.Syntax(), fn) } } func (c *Checker) CheckIneffectiveLoop(j *lint.Job) { // This check detects some, but not all unconditional loop exits. // We give up in the following cases: // // - a goto anywhere in the loop. The goto might skip over our // return, and we don't check that it doesn't. // // - any nested, unlabelled continue, even if it is in another // loop or closure. fn := func(node ast.Node) bool { var body *ast.BlockStmt switch fn := node.(type) { case *ast.FuncDecl: body = fn.Body case *ast.FuncLit: body = fn.Body default: return true } if body == nil { return true } labels := map[*ast.Object]ast.Stmt{} ast.Inspect(body, func(node ast.Node) bool { label, ok := node.(*ast.LabeledStmt) if !ok { return true } labels[label.Label.Obj] = label.Stmt return true }) ast.Inspect(body, func(node ast.Node) bool { var loop ast.Node var body *ast.BlockStmt switch node := node.(type) { case *ast.ForStmt: body = node.Body loop = node case *ast.RangeStmt: typ := TypeOf(j, node.X) if _, ok := typ.Underlying().(*types.Map); ok { // looping once over a map is a valid pattern for // getting an arbitrary element. return true } body = node.Body loop = node default: return true } if len(body.List) < 2 { // avoid flagging the somewhat common pattern of using // a range loop to get the first element in a slice, // or the first rune in a string. return true } var unconditionalExit ast.Node hasBranching := false for _, stmt := range body.List { switch stmt := stmt.(type) { case *ast.BranchStmt: switch stmt.Tok { case token.BREAK: if stmt.Label == nil || labels[stmt.Label.Obj] == loop { unconditionalExit = stmt } case token.CONTINUE: if stmt.Label == nil || labels[stmt.Label.Obj] == loop { unconditionalExit = nil return false } } case *ast.ReturnStmt: unconditionalExit = stmt case *ast.IfStmt, *ast.ForStmt, *ast.RangeStmt, *ast.SwitchStmt, *ast.SelectStmt: hasBranching = true } } if unconditionalExit == nil || !hasBranching { return false } ast.Inspect(body, func(node ast.Node) bool { if branch, ok := node.(*ast.BranchStmt); ok { switch branch.Tok { case token.GOTO: unconditionalExit = nil return false case token.CONTINUE: if branch.Label != nil && labels[branch.Label.Obj] != loop { return true } unconditionalExit = nil return false } } return true }) if unconditionalExit != nil { j.Errorf(unconditionalExit, "the surrounding loop is unconditionally terminated") } return true }) return true } for _, f := range j.Program.Files { ast.Inspect(f, fn) } } func (c *Checker) CheckNilContext(j *lint.Job) { fn := func(node ast.Node) bool { call, ok := node.(*ast.CallExpr) if !ok { return true } if len(call.Args) == 0 { return true } if typ, ok := TypeOf(j, call.Args[0]).(*types.Basic); !ok || typ.Kind() != types.UntypedNil { return true } sig, ok := TypeOf(j, call.Fun).(*types.Signature) if !ok { return true } if sig.Params().Len() == 0 { return true } if !IsType(sig.Params().At(0).Type(), "context.Context") { return true } j.Errorf(call.Args[0], "do not pass a nil Context, even if a function permits it; pass context.TODO if you are unsure about which Context to use") return true } for _, f := range j.Program.Files { ast.Inspect(f, fn) } } func (c *Checker) CheckSeeker(j *lint.Job) { fn := func(node ast.Node) bool { call, ok := node.(*ast.CallExpr) if !ok { return true } sel, ok := call.Fun.(*ast.SelectorExpr) if !ok { return true } if sel.Sel.Name != "Seek" { return true } if len(call.Args) != 2 { return true } arg0, ok := call.Args[0].(*ast.SelectorExpr) if !ok { return true } switch arg0.Sel.Name { case "SeekStart", "SeekCurrent", "SeekEnd": default: return true } pkg, ok := arg0.X.(*ast.Ident) if !ok { return true } if pkg.Name != "io" { return true } j.Errorf(call, "the first argument of io.Seeker is the offset, but an io.Seek* constant is being used instead") return true } for _, f := range j.Program.Files { ast.Inspect(f, fn) } } func (c *Checker) CheckIneffectiveAppend(j *lint.Job) { isAppend := func(ins ssa.Value) bool { call, ok := ins.(*ssa.Call) if !ok { return false } if call.Call.IsInvoke() { return false } if builtin, ok := call.Call.Value.(*ssa.Builtin); !ok || builtin.Name() != "append" { return false } return true } for _, ssafn := range j.Program.InitialFunctions { for _, block := range ssafn.Blocks { for _, ins := range block.Instrs { val, ok := ins.(ssa.Value) if !ok || !isAppend(val) { continue } isUsed := false visited := map[ssa.Instruction]bool{} var walkRefs func(refs []ssa.Instruction) walkRefs = func(refs []ssa.Instruction) { loop: for _, ref := range refs { if visited[ref] { continue } visited[ref] = true if _, ok := ref.(*ssa.DebugRef); ok { continue } switch ref := ref.(type) { case *ssa.Phi: walkRefs(*ref.Referrers()) case *ssa.Sigma: walkRefs(*ref.Referrers()) case ssa.Value: if !isAppend(ref) { isUsed = true } else { walkRefs(*ref.Referrers()) } case ssa.Instruction: isUsed = true break loop } } } refs := val.Referrers() if refs == nil { continue } walkRefs(*refs) if !isUsed { j.Errorf(ins, "this result of append is never used, except maybe in other appends") } } } } } func (c *Checker) CheckConcurrentTesting(j *lint.Job) { for _, ssafn := range j.Program.InitialFunctions { for _, block := range ssafn.Blocks { for _, ins := range block.Instrs { gostmt, ok := ins.(*ssa.Go) if !ok { continue } var fn *ssa.Function switch val := gostmt.Call.Value.(type) { case *ssa.Function: fn = val case *ssa.MakeClosure: fn = val.Fn.(*ssa.Function) default: continue } if fn.Blocks == nil { continue } for _, block := range fn.Blocks { for _, ins := range block.Instrs { call, ok := ins.(*ssa.Call) if !ok { continue } if call.Call.IsInvoke() { continue } callee := call.Call.StaticCallee() if callee == nil { continue } recv := callee.Signature.Recv() if recv == nil { continue } if !IsType(recv.Type(), "*testing.common") { continue } fn, ok := call.Call.StaticCallee().Object().(*types.Func) if !ok { continue } name := fn.Name() switch name { case "FailNow", "Fatal", "Fatalf", "SkipNow", "Skip", "Skipf": default: continue } j.Errorf(gostmt, "the goroutine calls T.%s, which must be called in the same goroutine as the test", name) } } } } } } func (c *Checker) CheckCyclicFinalizer(j *lint.Job) { for _, ssafn := range j.Program.InitialFunctions { node := c.funcDescs.CallGraph.CreateNode(ssafn) for _, edge := range node.Out { if edge.Callee.Func.RelString(nil) != "runtime.SetFinalizer" { continue } arg0 := edge.Site.Common().Args[0] if iface, ok := arg0.(*ssa.MakeInterface); ok { arg0 = iface.X } unop, ok := arg0.(*ssa.UnOp) if !ok { continue } v, ok := unop.X.(*ssa.Alloc) if !ok { continue } arg1 := edge.Site.Common().Args[1] if iface, ok := arg1.(*ssa.MakeInterface); ok { arg1 = iface.X } mc, ok := arg1.(*ssa.MakeClosure) if !ok { continue } for _, b := range mc.Bindings { if b == v { pos := j.Program.DisplayPosition(mc.Fn.Pos()) j.Errorf(edge.Site, "the finalizer closes over the object, preventing the finalizer from ever running (at %s)", pos) } } } } } func (c *Checker) CheckSliceOutOfBounds(j *lint.Job) { for _, ssafn := range j.Program.InitialFunctions { for _, block := range ssafn.Blocks { for _, ins := range block.Instrs { ia, ok := ins.(*ssa.IndexAddr) if !ok { continue } if _, ok := ia.X.Type().Underlying().(*types.Slice); !ok { continue } sr, ok1 := c.funcDescs.Get(ssafn).Ranges[ia.X].(vrp.SliceInterval) idxr, ok2 := c.funcDescs.Get(ssafn).Ranges[ia.Index].(vrp.IntInterval) if !ok1 || !ok2 || !sr.IsKnown() || !idxr.IsKnown() || sr.Length.Empty() || idxr.Empty() { continue } if idxr.Lower.Cmp(sr.Length.Upper) >= 0 { j.Errorf(ia, "index out of bounds") } } } } } func (c *Checker) CheckDeferLock(j *lint.Job) { for _, ssafn := range j.Program.InitialFunctions { for _, block := range ssafn.Blocks { instrs := FilterDebug(block.Instrs) if len(instrs) < 2 { continue } for i, ins := range instrs[:len(instrs)-1] { call, ok := ins.(*ssa.Call) if !ok { continue } if !IsCallTo(call.Common(), "(*sync.Mutex).Lock") && !IsCallTo(call.Common(), "(*sync.RWMutex).RLock") { continue } nins, ok := instrs[i+1].(*ssa.Defer) if !ok { continue } if !IsCallTo(&nins.Call, "(*sync.Mutex).Lock") && !IsCallTo(&nins.Call, "(*sync.RWMutex).RLock") { continue } if call.Common().Args[0] != nins.Call.Args[0] { continue } name := shortCallName(call.Common()) alt := "" switch name { case "Lock": alt = "Unlock" case "RLock": alt = "RUnlock" } j.Errorf(nins, "deferring %s right after having locked already; did you mean to defer %s?", name, alt) } } } } func (c *Checker) CheckNaNComparison(j *lint.Job) { isNaN := func(v ssa.Value) bool { call, ok := v.(*ssa.Call) if !ok { return false } return IsCallTo(call.Common(), "math.NaN") } for _, ssafn := range j.Program.InitialFunctions { for _, block := range ssafn.Blocks { for _, ins := range block.Instrs { ins, ok := ins.(*ssa.BinOp) if !ok { continue } if isNaN(ins.X) || isNaN(ins.Y) { j.Errorf(ins, "no value is equal to NaN, not even NaN itself") } } } } } func (c *Checker) CheckInfiniteRecursion(j *lint.Job) { for _, ssafn := range j.Program.InitialFunctions { node := c.funcDescs.CallGraph.CreateNode(ssafn) for _, edge := range node.Out { if edge.Callee != node { continue } if _, ok := edge.Site.(*ssa.Go); ok { // Recursively spawning goroutines doesn't consume // stack space infinitely, so don't flag it. continue } block := edge.Site.Block() canReturn := false for _, b := range ssafn.Blocks { if block.Dominates(b) { continue } if len(b.Instrs) == 0 { continue } if _, ok := b.Instrs[len(b.Instrs)-1].(*ssa.Return); ok { canReturn = true break } } if canReturn { continue } j.Errorf(edge.Site, "infinite recursive call") } } } func objectName(obj types.Object) string { if obj == nil { return "" } var name string if obj.Pkg() != nil && obj.Pkg().Scope().Lookup(obj.Name()) == obj { var s string s = obj.Pkg().Path() if s != "" { name += s + "." } } name += obj.Name() return name } func isName(j *lint.Job, expr ast.Expr, name string) bool { var obj types.Object switch expr := expr.(type) { case *ast.Ident: obj = ObjectOf(j, expr) case *ast.SelectorExpr: obj = ObjectOf(j, expr.Sel) } return objectName(obj) == name } func (c *Checker) CheckLeakyTimeTick(j *lint.Job) { for _, ssafn := range j.Program.InitialFunctions { if IsInMain(j, ssafn) || IsInTest(j, ssafn) { continue } for _, block := range ssafn.Blocks { for _, ins := range block.Instrs { call, ok := ins.(*ssa.Call) if !ok || !IsCallTo(call.Common(), "time.Tick") { continue } if c.funcDescs.Get(call.Parent()).Infinite { continue } j.Errorf(call, "using time.Tick leaks the underlying ticker, consider using it only in endless functions, tests and the main package, and use time.NewTicker here") } } } } func (c *Checker) CheckDoubleNegation(j *lint.Job) { fn := func(node ast.Node) bool { unary1, ok := node.(*ast.UnaryExpr) if !ok { return true } unary2, ok := unary1.X.(*ast.UnaryExpr) if !ok { return true } if unary1.Op != token.NOT || unary2.Op != token.NOT { return true } j.Errorf(unary1, "negating a boolean twice has no effect; is this a typo?") return true } for _, f := range j.Program.Files { ast.Inspect(f, fn) } } func hasSideEffects(node ast.Node) bool { dynamic := false ast.Inspect(node, func(node ast.Node) bool { switch node := node.(type) { case *ast.CallExpr: dynamic = true return false case *ast.UnaryExpr: if node.Op == token.ARROW { dynamic = true return false } } return true }) return dynamic } func (c *Checker) CheckRepeatedIfElse(j *lint.Job) { seen := map[ast.Node]bool{} var collectConds func(ifstmt *ast.IfStmt, inits []ast.Stmt, conds []ast.Expr) ([]ast.Stmt, []ast.Expr) collectConds = func(ifstmt *ast.IfStmt, inits []ast.Stmt, conds []ast.Expr) ([]ast.Stmt, []ast.Expr) { seen[ifstmt] = true if ifstmt.Init != nil { inits = append(inits, ifstmt.Init) } conds = append(conds, ifstmt.Cond) if elsestmt, ok := ifstmt.Else.(*ast.IfStmt); ok { return collectConds(elsestmt, inits, conds) } return inits, conds } fn := func(node ast.Node) bool { ifstmt, ok := node.(*ast.IfStmt) if !ok { return true } if seen[ifstmt] { return true } inits, conds := collectConds(ifstmt, nil, nil) if len(inits) > 0 { return true } for _, cond := range conds { if hasSideEffects(cond) { return true } } counts := map[string]int{} for _, cond := range conds { s := Render(j, cond) counts[s]++ if counts[s] == 2 { j.Errorf(cond, "this condition occurs multiple times in this if/else if chain") } } return true } for _, f := range j.Program.Files { ast.Inspect(f, fn) } } func (c *Checker) CheckSillyBitwiseOps(j *lint.Job) { for _, ssafn := range j.Program.InitialFunctions { for _, block := range ssafn.Blocks { for _, ins := range block.Instrs { ins, ok := ins.(*ssa.BinOp) if !ok { continue } if c, ok := ins.Y.(*ssa.Const); !ok || c.Value == nil || c.Value.Kind() != constant.Int || c.Uint64() != 0 { continue } switch ins.Op { case token.AND, token.OR, token.XOR: default: // we do not flag shifts because too often, x<<0 is part // of a pattern, x<<0, x<<8, x<<16, ... continue } path, _ := astutil.PathEnclosingInterval(j.File(ins), ins.Pos(), ins.Pos()) if len(path) == 0 { continue } if node, ok := path[0].(*ast.BinaryExpr); !ok || !IsZero(node.Y) { continue } switch ins.Op { case token.AND: j.Errorf(ins, "x & 0 always equals 0") case token.OR, token.XOR: j.Errorf(ins, "x %s 0 always equals x", ins.Op) } } } } } func (c *Checker) CheckNonOctalFileMode(j *lint.Job) { fn := func(node ast.Node) bool { call, ok := node.(*ast.CallExpr) if !ok { return true } sig, ok := TypeOf(j, call.Fun).(*types.Signature) if !ok { return true } n := sig.Params().Len() var args []int for i := 0; i < n; i++ { typ := sig.Params().At(i).Type() if IsType(typ, "os.FileMode") { args = append(args, i) } } for _, i := range args { lit, ok := call.Args[i].(*ast.BasicLit) if !ok { continue } if len(lit.Value) == 3 && lit.Value[0] != '0' && lit.Value[0] >= '0' && lit.Value[0] <= '7' && lit.Value[1] >= '0' && lit.Value[1] <= '7' && lit.Value[2] >= '0' && lit.Value[2] <= '7' { v, err := strconv.ParseInt(lit.Value, 10, 64) if err != nil { continue } j.Errorf(call.Args[i], "file mode '%s' evaluates to %#o; did you mean '0%s'?", lit.Value, v, lit.Value) } } return true } for _, f := range j.Program.Files { ast.Inspect(f, fn) } } func (c *Checker) CheckPureFunctions(j *lint.Job) { fnLoop: for _, ssafn := range j.Program.InitialFunctions { if IsInTest(j, ssafn) { params := ssafn.Signature.Params() for i := 0; i < params.Len(); i++ { param := params.At(i) if IsType(param.Type(), "*testing.B") { // Ignore discarded pure functions in code related // to benchmarks. Instead of matching BenchmarkFoo // functions, we match any function accepting a // *testing.B. Benchmarks sometimes call generic // functions for doing the actual work, and // checking for the parameter is a lot easier and // faster than analyzing call trees. continue fnLoop } } } for _, b := range ssafn.Blocks { for _, ins := range b.Instrs { ins, ok := ins.(*ssa.Call) if !ok { continue } refs := ins.Referrers() if refs == nil || len(FilterDebug(*refs)) > 0 { continue } callee := ins.Common().StaticCallee() if callee == nil { continue } if c.funcDescs.Get(callee).Pure && !c.funcDescs.Get(callee).Stub { j.Errorf(ins, "%s is a pure function but its return value is ignored", callee.Name()) continue } } } } } func (c *Checker) isDeprecated(j *lint.Job, ident *ast.Ident) (bool, string) { obj := ObjectOf(j, ident) if obj.Pkg() == nil { return false, "" } alt := c.deprecatedObjs[obj] return alt != "", alt } func (c *Checker) CheckDeprecated(j *lint.Job) { // Selectors can appear outside of function literals, e.g. when // declaring package level variables. var ssafn *ssa.Function stack := 0 fn := func(node ast.Node) bool { if node == nil { stack-- } else { stack++ } if stack == 1 { ssafn = nil } if fn, ok := node.(*ast.FuncDecl); ok { ssafn = j.Program.SSA.FuncValue(ObjectOf(j, fn.Name).(*types.Func)) } sel, ok := node.(*ast.SelectorExpr) if !ok { return true } obj := ObjectOf(j, sel.Sel) if obj.Pkg() == nil { return true } nodePkg := j.NodePackage(node).Pkg if nodePkg == obj.Pkg() || obj.Pkg().Path()+"_test" == nodePkg.Path() { // Don't flag stuff in our own package return true } if ok, alt := c.isDeprecated(j, sel.Sel); ok { // Look for the first available alternative, not the first // version something was deprecated in. If a function was // deprecated in Go 1.6, an alternative has been available // already in 1.0, and we're targeting 1.2, it still // makes sense to use the alternative from 1.0, to be // future-proof. minVersion := deprecated.Stdlib[SelectorName(j, sel)].AlternativeAvailableSince if !IsGoVersion(j, minVersion) { return true } if ssafn != nil { if _, ok := c.deprecatedObjs[ssafn.Object()]; ok { // functions that are deprecated may use deprecated // symbols return true } } j.Errorf(sel, "%s is deprecated: %s", Render(j, sel), alt) return true } return true } for _, f := range j.Program.Files { ast.Inspect(f, fn) } } func (c *Checker) callChecker(rules map[string]CallCheck) func(j *lint.Job) { return func(j *lint.Job) { c.checkCalls(j, rules) } } func (c *Checker) checkCalls(j *lint.Job, rules map[string]CallCheck) { for _, ssafn := range j.Program.InitialFunctions { node := c.funcDescs.CallGraph.CreateNode(ssafn) for _, edge := range node.Out { callee := edge.Callee.Func obj, ok := callee.Object().(*types.Func) if !ok { continue } r, ok := rules[obj.FullName()] if !ok { continue } var args []*Argument ssaargs := edge.Site.Common().Args if callee.Signature.Recv() != nil { ssaargs = ssaargs[1:] } for _, arg := range ssaargs { if iarg, ok := arg.(*ssa.MakeInterface); ok { arg = iarg.X } vr := c.funcDescs.Get(edge.Site.Parent()).Ranges[arg] args = append(args, &Argument{Value: Value{arg, vr}}) } call := &Call{ Job: j, Instr: edge.Site, Args: args, Checker: c, Parent: edge.Site.Parent(), } r(call) for idx, arg := range call.Args { _ = idx for _, e := range arg.invalids { // path, _ := astutil.PathEnclosingInterval(f.File, edge.Site.Pos(), edge.Site.Pos()) // if len(path) < 2 { // continue // } // astcall, ok := path[0].(*ast.CallExpr) // if !ok { // continue // } // j.Errorf(astcall.Args[idx], "%s", e) j.Errorf(edge.Site, "%s", e) } } for _, e := range call.invalids { j.Errorf(call.Instr.Common(), "%s", e) } } } } func unwrapFunction(val ssa.Value) *ssa.Function { switch val := val.(type) { case *ssa.Function: return val case *ssa.MakeClosure: return val.Fn.(*ssa.Function) default: return nil } } func shortCallName(call *ssa.CallCommon) string { if call.IsInvoke() { return "" } switch v := call.Value.(type) { case *ssa.Function: fn, ok := v.Object().(*types.Func) if !ok { return "" } return fn.Name() case *ssa.Builtin: return v.Name() } return "" } func hasCallTo(block *ssa.BasicBlock, name string) bool { for _, ins := range block.Instrs { call, ok := ins.(*ssa.Call) if !ok { continue } if IsCallTo(call.Common(), name) { return true } } return false } func (c *Checker) CheckWriterBufferModified(j *lint.Job) { // TODO(dh): this might be a good candidate for taint analysis. // Taint the argument as MUST_NOT_MODIFY, then propagate that // through functions like bytes.Split for _, ssafn := range j.Program.InitialFunctions { sig := ssafn.Signature if ssafn.Name() != "Write" || sig.Recv() == nil || sig.Params().Len() != 1 || sig.Results().Len() != 2 { continue } tArg, ok := sig.Params().At(0).Type().(*types.Slice) if !ok { continue } if basic, ok := tArg.Elem().(*types.Basic); !ok || basic.Kind() != types.Byte { continue } if basic, ok := sig.Results().At(0).Type().(*types.Basic); !ok || basic.Kind() != types.Int { continue } if named, ok := sig.Results().At(1).Type().(*types.Named); !ok || !IsType(named, "error") { continue } for _, block := range ssafn.Blocks { for _, ins := range block.Instrs { switch ins := ins.(type) { case *ssa.Store: addr, ok := ins.Addr.(*ssa.IndexAddr) if !ok { continue } if addr.X != ssafn.Params[1] { continue } j.Errorf(ins, "io.Writer.Write must not modify the provided buffer, not even temporarily") case *ssa.Call: if !IsCallTo(ins.Common(), "append") { continue } if ins.Common().Args[0] != ssafn.Params[1] { continue } j.Errorf(ins, "io.Writer.Write must not modify the provided buffer, not even temporarily") } } } } } func loopedRegexp(name string) CallCheck { return func(call *Call) { if len(extractConsts(call.Args[0].Value.Value)) == 0 { return } if !call.Checker.isInLoop(call.Instr.Block()) { return } call.Invalid(fmt.Sprintf("calling %s in a loop has poor performance, consider using regexp.Compile", name)) } } func (c *Checker) CheckEmptyBranch(j *lint.Job) { for _, ssafn := range j.Program.InitialFunctions { if ssafn.Syntax() == nil { continue } if IsGenerated(j.File(ssafn.Syntax())) { continue } if IsExample(ssafn) { continue } fn := func(node ast.Node) bool { ifstmt, ok := node.(*ast.IfStmt) if !ok { return true } if ifstmt.Else != nil { b, ok := ifstmt.Else.(*ast.BlockStmt) if !ok || len(b.List) != 0 { return true } j.Errorf(ifstmt.Else, "empty branch") } if len(ifstmt.Body.List) != 0 { return true } j.Errorf(ifstmt, "empty branch") return true } Inspect(ssafn.Syntax(), fn) } } func (c *Checker) CheckMapBytesKey(j *lint.Job) { for _, fn := range j.Program.InitialFunctions { for _, b := range fn.Blocks { insLoop: for _, ins := range b.Instrs { // find []byte -> string conversions conv, ok := ins.(*ssa.Convert) if !ok || conv.Type() != types.Universe.Lookup("string").Type() { continue } if s, ok := conv.X.Type().(*types.Slice); !ok || s.Elem() != types.Universe.Lookup("byte").Type() { continue } refs := conv.Referrers() // need at least two (DebugRef) references: the // conversion and the *ast.Ident if refs == nil || len(*refs) < 2 { continue } ident := false // skip first reference, that's the conversion itself for _, ref := range (*refs)[1:] { switch ref := ref.(type) { case *ssa.DebugRef: if _, ok := ref.Expr.(*ast.Ident); !ok { // the string seems to be used somewhere // unexpected; the default branch should // catch this already, but be safe continue insLoop } else { ident = true } case *ssa.Lookup: default: // the string is used somewhere else than a // map lookup continue insLoop } } // the result of the conversion wasn't assigned to an // identifier if !ident { continue } j.Errorf(conv, "m[string(key)] would be more efficient than k := string(key); m[k]") } } } } func (c *Checker) CheckRangeStringRunes(j *lint.Job) { sharedcheck.CheckRangeStringRunes(j) } func (c *Checker) CheckSelfAssignment(j *lint.Job) { fn := func(node ast.Node) bool { assign, ok := node.(*ast.AssignStmt) if !ok { return true } if assign.Tok != token.ASSIGN || len(assign.Lhs) != len(assign.Rhs) { return true } for i, stmt := range assign.Lhs { rlh := Render(j, stmt) rrh := Render(j, assign.Rhs[i]) if rlh == rrh { j.Errorf(assign, "self-assignment of %s to %s", rrh, rlh) } } return true } for _, f := range c.filterGenerated(j.Program.Files) { ast.Inspect(f, fn) } } func buildTagsIdentical(s1, s2 []string) bool { if len(s1) != len(s2) { return false } s1s := make([]string, len(s1)) copy(s1s, s1) sort.Strings(s1s) s2s := make([]string, len(s2)) copy(s2s, s2) sort.Strings(s2s) for i, s := range s1s { if s != s2s[i] { return false } } return true } func (c *Checker) CheckDuplicateBuildConstraints(job *lint.Job) { for _, f := range c.filterGenerated(job.Program.Files) { constraints := buildTags(f) for i, constraint1 := range constraints { for j, constraint2 := range constraints { if i >= j { continue } if buildTagsIdentical(constraint1, constraint2) { job.Errorf(f, "identical build constraints %q and %q", strings.Join(constraint1, " "), strings.Join(constraint2, " ")) } } } } } func (c *Checker) CheckSillyRegexp(j *lint.Job) { // We could use the rule checking engine for this, but the // arguments aren't really invalid. for _, fn := range j.Program.InitialFunctions { for _, b := range fn.Blocks { for _, ins := range b.Instrs { call, ok := ins.(*ssa.Call) if !ok { continue } switch CallName(call.Common()) { case "regexp.MustCompile", "regexp.Compile", "regexp.Match", "regexp.MatchReader", "regexp.MatchString": default: continue } c, ok := call.Common().Args[0].(*ssa.Const) if !ok { continue } s := constant.StringVal(c.Value) re, err := syntax.Parse(s, 0) if err != nil { continue } if re.Op != syntax.OpLiteral && re.Op != syntax.OpEmptyMatch { continue } j.Errorf(call, "regular expression does not contain any meta characters") } } } } func (c *Checker) CheckMissingEnumTypesInDeclaration(j *lint.Job) { fn := func(node ast.Node) bool { decl, ok := node.(*ast.GenDecl) if !ok { return true } if !decl.Lparen.IsValid() { // not a parenthesised gendecl // // TODO(dh): do we need this check, considering we require // decl.Specs to contain 2+ elements? return true } if decl.Tok != token.CONST { return true } if len(decl.Specs) < 2 { return true } if decl.Specs[0].(*ast.ValueSpec).Type == nil { // first constant doesn't have a type return true } for i, spec := range decl.Specs { spec := spec.(*ast.ValueSpec) if len(spec.Names) != 1 || len(spec.Values) != 1 { return true } switch v := spec.Values[0].(type) { case *ast.BasicLit: case *ast.UnaryExpr: if _, ok := v.X.(*ast.BasicLit); !ok { return true } default: // if it's not a literal it might be typed, such as // time.Microsecond = 1000 * Nanosecond return true } if i == 0 { continue } if spec.Type != nil { return true } } j.Errorf(decl, "only the first constant has an explicit type") return true } for _, f := range j.Program.Files { ast.Inspect(f, fn) } }