Add staticcheck tool

This commit is contained in:
dwrz
2026-06-06 01:17:20 +00:00
parent 6fb63c8c90
commit ad58cd78ff
315 changed files with 56337 additions and 4 deletions

View File

@@ -0,0 +1,80 @@
// Code generated by generate.go. DO NOT EDIT.
package simple
import (
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/simple/s1000"
"honnef.co/go/tools/simple/s1001"
"honnef.co/go/tools/simple/s1002"
"honnef.co/go/tools/simple/s1003"
"honnef.co/go/tools/simple/s1004"
"honnef.co/go/tools/simple/s1005"
"honnef.co/go/tools/simple/s1006"
"honnef.co/go/tools/simple/s1007"
"honnef.co/go/tools/simple/s1008"
"honnef.co/go/tools/simple/s1009"
"honnef.co/go/tools/simple/s1010"
"honnef.co/go/tools/simple/s1011"
"honnef.co/go/tools/simple/s1012"
"honnef.co/go/tools/simple/s1016"
"honnef.co/go/tools/simple/s1017"
"honnef.co/go/tools/simple/s1018"
"honnef.co/go/tools/simple/s1019"
"honnef.co/go/tools/simple/s1020"
"honnef.co/go/tools/simple/s1021"
"honnef.co/go/tools/simple/s1023"
"honnef.co/go/tools/simple/s1024"
"honnef.co/go/tools/simple/s1025"
"honnef.co/go/tools/simple/s1028"
"honnef.co/go/tools/simple/s1029"
"honnef.co/go/tools/simple/s1030"
"honnef.co/go/tools/simple/s1031"
"honnef.co/go/tools/simple/s1032"
"honnef.co/go/tools/simple/s1033"
"honnef.co/go/tools/simple/s1034"
"honnef.co/go/tools/simple/s1035"
"honnef.co/go/tools/simple/s1036"
"honnef.co/go/tools/simple/s1037"
"honnef.co/go/tools/simple/s1038"
"honnef.co/go/tools/simple/s1039"
"honnef.co/go/tools/simple/s1040"
)
var Analyzers = []*lint.Analyzer{
s1000.SCAnalyzer,
s1001.SCAnalyzer,
s1002.SCAnalyzer,
s1003.SCAnalyzer,
s1004.SCAnalyzer,
s1005.SCAnalyzer,
s1006.SCAnalyzer,
s1007.SCAnalyzer,
s1008.SCAnalyzer,
s1009.SCAnalyzer,
s1010.SCAnalyzer,
s1011.SCAnalyzer,
s1012.SCAnalyzer,
s1016.SCAnalyzer,
s1017.SCAnalyzer,
s1018.SCAnalyzer,
s1019.SCAnalyzer,
s1020.SCAnalyzer,
s1021.SCAnalyzer,
s1023.SCAnalyzer,
s1024.SCAnalyzer,
s1025.SCAnalyzer,
s1028.SCAnalyzer,
s1029.SCAnalyzer,
s1030.SCAnalyzer,
s1031.SCAnalyzer,
s1032.SCAnalyzer,
s1033.SCAnalyzer,
s1034.SCAnalyzer,
s1035.SCAnalyzer,
s1036.SCAnalyzer,
s1037.SCAnalyzer,
s1038.SCAnalyzer,
s1039.SCAnalyzer,
s1040.SCAnalyzer,
}

View File

@@ -0,0 +1,6 @@
//go:generate go run ../generate.go
// Package simple contains analyzes that simplify code.
// All suggestions made by these analyzes are intended to result in objectively simpler code,
// and following their advice is recommended.
package simple

View File

@@ -0,0 +1,71 @@
package s1000
import (
"go/ast"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1000",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer, generated.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Use plain channel send or receive instead of single-case select`,
Text: `Select statements with a single case can be replaced with a simple
send or receive.`,
Before: `
select {
case x := <-ch:
fmt.Println(x)
}`,
After: `
x := <-ch
fmt.Println(x)
`,
Since: "2017.1",
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var (
checkSingleCaseSelectQ1 = pattern.MustParse(`
(ForStmt
nil nil nil
select@(SelectStmt
(CommClause
(Or
(UnaryExpr "<-" _)
(AssignStmt _ _ (UnaryExpr "<-" _)))
_)))`)
checkSingleCaseSelectQ2 = pattern.MustParse(`(SelectStmt (CommClause _ _))`)
)
func run(pass *analysis.Pass) (any, error) {
seen := map[ast.Node]struct{}{}
fn := func(node ast.Node) {
if m, ok := code.Match(pass, checkSingleCaseSelectQ1, node); ok {
seen[m.State["select"].(ast.Node)] = struct{}{}
report.Report(pass, node, "should use for range instead of for { select {} }", report.FilterGenerated())
} else if _, ok := code.Match(pass, checkSingleCaseSelectQ2, node); ok {
if _, ok := seen[node]; !ok {
report.Report(pass, node, "should use a simple channel send/receive instead of select with a single case",
report.ShortRange(),
report.FilterGenerated())
}
}
}
code.Preorder(pass, fn, (*ast.ForStmt)(nil), (*ast.SelectStmt)(nil))
return nil, nil
}

View File

@@ -0,0 +1,190 @@
package s1001
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1001",
Run: run,
Requires: append([]*analysis.Analyzer{generated.Analyzer}, code.RequiredAnalyzers...),
},
Doc: &lint.RawDocumentation{
Title: `Replace for loop with call to copy`,
Text: `
Use \'copy()\' for copying elements from one slice to another. For
arrays of identical size, you can use simple assignment.`,
Before: `
for i, x := range src {
dst[i] = x
}`,
After: `copy(dst, src)`,
Since: "2017.1",
// MergeIfAll because the types of src and dst might be different under different build tags.
// You shouldn't write code like that…
MergeIf: lint.MergeIfAll,
},
})
var Analyzer = SCAnalyzer.Analyzer
var (
checkLoopCopyQ = pattern.MustParse(`
(Or
(RangeStmt
key@(Ident _) value@(Ident _) ":=" src
[(AssignStmt (IndexExpr dst key) "=" value)])
(RangeStmt
key@(Ident _) nil ":=" src
[(AssignStmt (IndexExpr dst key) "=" (IndexExpr src key))])
(ForStmt
(AssignStmt key@(Ident _) ":=" (IntegerLiteral "0"))
(BinaryExpr key "<" (CallExpr (Symbol "len") [src]))
(IncDecStmt key "++")
[(AssignStmt (IndexExpr dst key) "=" (IndexExpr src key))]))`)
)
func run(pass *analysis.Pass) (any, error) {
// TODO revisit once range doesn't require a structural type
isInvariant := func(k, v types.Object, node ast.Expr) bool {
if code.MayHaveSideEffects(pass, node, nil) {
return false
}
invariant := true
ast.Inspect(node, func(node ast.Node) bool {
if node, ok := node.(*ast.Ident); ok {
obj := pass.TypesInfo.ObjectOf(node)
if obj == k || obj == v {
// don't allow loop bodies like 'a[i][i] = v'
invariant = false
return false
}
}
return true
})
return invariant
}
var elType func(T types.Type) (el types.Type, isArray bool, isArrayPointer bool, ok bool)
elType = func(T types.Type) (el types.Type, isArray bool, isArrayPointer bool, ok bool) {
switch typ := T.Underlying().(type) {
case *types.Slice:
return typ.Elem(), false, false, true
case *types.Array:
return typ.Elem(), true, false, true
case *types.Pointer:
el, isArray, _, ok = elType(typ.Elem())
return el, isArray, true, ok
default:
return nil, false, false, false
}
}
for node, m := range code.Matches(pass, checkLoopCopyQ) {
src := m.State["src"].(ast.Expr)
dst := m.State["dst"].(ast.Expr)
k := pass.TypesInfo.ObjectOf(m.State["key"].(*ast.Ident))
var v types.Object
if value, ok := m.State["value"]; ok {
v = pass.TypesInfo.ObjectOf(value.(*ast.Ident))
}
if !isInvariant(k, v, dst) {
continue
}
if !isInvariant(k, v, src) {
// For example: 'for i := range foo()'
continue
}
Tsrc := pass.TypesInfo.TypeOf(src)
Tdst := pass.TypesInfo.TypeOf(dst)
TsrcElem, TsrcArray, TsrcPointer, ok := elType(Tsrc)
if !ok {
continue
}
if TsrcPointer {
Tsrc = Tsrc.Underlying().(*types.Pointer).Elem()
}
TdstElem, TdstArray, TdstPointer, ok := elType(Tdst)
if !ok {
continue
}
if TdstPointer {
Tdst = Tdst.Underlying().(*types.Pointer).Elem()
}
if !types.Identical(TsrcElem, TdstElem) {
continue
}
if TsrcArray && TdstArray && types.Identical(Tsrc, Tdst) {
if TsrcPointer {
src = &ast.StarExpr{
X: src,
}
}
if TdstPointer {
dst = &ast.StarExpr{
X: dst,
}
}
r := &ast.AssignStmt{
Lhs: []ast.Expr{dst},
Rhs: []ast.Expr{src},
Tok: token.ASSIGN,
}
report.Report(pass, node, "should copy arrays using assignment instead of using a loop",
report.FilterGenerated(),
report.ShortRange(),
report.Fixes(edit.Fix("Replace loop with assignment", edit.ReplaceWithNode(pass.Fset, node, r))))
} else {
tv, err := types.Eval(pass.Fset, pass.Pkg, node.Pos(), "copy")
if err == nil && tv.IsBuiltin() {
to := "to"
from := "from"
src := m.State["src"].(ast.Expr)
if TsrcArray {
from = "from[:]"
src = &ast.SliceExpr{
X: src,
}
}
dst := m.State["dst"].(ast.Expr)
if TdstArray {
to = "to[:]"
dst = &ast.SliceExpr{
X: dst,
}
}
r := &ast.CallExpr{
Fun: &ast.Ident{Name: "copy"},
Args: []ast.Expr{dst, src},
}
opts := []report.Option{
report.ShortRange(),
report.FilterGenerated(),
report.Fixes(edit.Fix("Replace loop with call to copy()", edit.ReplaceWithNode(pass.Fset, node, r))),
}
report.Report(pass, node, fmt.Sprintf("should use copy(%s, %s) instead of a loop", to, from), opts...)
}
}
}
return nil, nil
}

View File

@@ -0,0 +1,88 @@
package s1002
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"strings"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/types/typeutil"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1002",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer, generated.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Omit comparison with boolean constant`,
Before: `if x == true {}`,
After: `if x {}`,
Since: "2017.1",
// MergeIfAll because 'true' might not be the builtin constant under all build tags.
// You shouldn't write code like that…
MergeIf: lint.MergeIfAll,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
fn := func(node ast.Node) {
if code.IsInTest(pass, node) {
return
}
expr := node.(*ast.BinaryExpr)
if expr.Op != token.EQL && expr.Op != token.NEQ {
return
}
x := code.IsBoolConst(pass, expr.X)
y := code.IsBoolConst(pass, expr.Y)
if !x && !y {
return
}
var other ast.Expr
var val bool
if x {
val = code.BoolConst(pass, expr.X)
other = expr.Y
} else {
val = code.BoolConst(pass, expr.Y)
other = expr.X
}
ok := typeutil.All(pass.TypesInfo.TypeOf(other), func(term *types.Term) bool {
basic, ok := term.Type().Underlying().(*types.Basic)
return ok && basic.Kind() == types.Bool
})
if !ok {
return
}
op := ""
if (expr.Op == token.EQL && !val) || (expr.Op == token.NEQ && val) {
op = "!"
}
r := op + report.Render(pass, other)
l1 := len(r)
r = strings.TrimLeft(r, "!")
if (l1-len(r))%2 == 1 {
r = "!" + r
}
report.Report(pass, expr, fmt.Sprintf("should omit comparison to bool constant, can be simplified to %s", r),
report.FilterGenerated(),
report.Fixes(edit.Fix("Simplify bool comparison", edit.ReplaceWithString(expr, r))))
}
code.Preorder(pass, fn, (*ast.BinaryExpr)(nil))
return nil, nil
}

View File

@@ -0,0 +1,118 @@
package s1003
import (
"fmt"
"go/ast"
"go/token"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1003",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer, generated.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Replace call to \'strings.Index\' with \'strings.Contains\'`,
Before: `if strings.Index(x, y) != -1 {}`,
After: `if strings.Contains(x, y) {}`,
Since: "2017.1",
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
// map of value to token to bool value
allowed := map[int64]map[token.Token]bool{
-1: {token.GTR: true, token.NEQ: true, token.EQL: false},
0: {token.GEQ: true, token.LSS: false},
}
fn := func(node ast.Node) {
expr := node.(*ast.BinaryExpr)
switch expr.Op {
case token.GEQ, token.GTR, token.NEQ, token.LSS, token.EQL:
default:
return
}
value, ok := code.ExprToInt(pass, expr.Y)
if !ok {
return
}
allowedOps, ok := allowed[value]
if !ok {
return
}
b, ok := allowedOps[expr.Op]
if !ok {
return
}
call, ok := expr.X.(*ast.CallExpr)
if !ok {
return
}
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
return
}
pkgIdent, ok := sel.X.(*ast.Ident)
if !ok {
return
}
funIdent := sel.Sel
if pkgIdent.Name != "strings" && pkgIdent.Name != "bytes" {
return
}
var r ast.Expr
switch funIdent.Name {
case "IndexRune":
r = &ast.SelectorExpr{
X: pkgIdent,
Sel: &ast.Ident{Name: "ContainsRune"},
}
case "IndexAny":
r = &ast.SelectorExpr{
X: pkgIdent,
Sel: &ast.Ident{Name: "ContainsAny"},
}
case "Index":
r = &ast.SelectorExpr{
X: pkgIdent,
Sel: &ast.Ident{Name: "Contains"},
}
default:
return
}
r = &ast.CallExpr{
Fun: r,
Args: call.Args,
}
if !b {
r = &ast.UnaryExpr{
Op: token.NOT,
X: r,
}
}
report.Report(pass, node, fmt.Sprintf("should use %s instead", report.Render(pass, r)),
report.FilterGenerated(),
report.Fixes(edit.Fix(fmt.Sprintf("Simplify use of %s", report.Render(pass, call.Fun)), edit.ReplaceWithNode(pass.Fset, node, r))))
}
code.Preorder(pass, fn, (*ast.BinaryExpr)(nil))
return nil, nil
}

View File

@@ -0,0 +1,65 @@
package s1004
import (
"fmt"
"go/ast"
"go/token"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1004",
Run: CheckBytesCompare,
Requires: append([]*analysis.Analyzer{generated.Analyzer}, code.RequiredAnalyzers...),
},
Doc: &lint.RawDocumentation{
Title: `Replace call to \'bytes.Compare\' with \'bytes.Equal\'`,
Before: `if bytes.Compare(x, y) == 0 {}`,
After: `if bytes.Equal(x, y) {}`,
Since: "2017.1",
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var (
checkBytesCompareQ = pattern.MustParse(`(BinaryExpr (CallExpr (Symbol "bytes.Compare") args) op@(Or "==" "!=") (IntegerLiteral "0"))`)
checkBytesCompareRe = pattern.MustParse(`(CallExpr (SelectorExpr (Ident "bytes") (Ident "Equal")) args)`)
checkBytesCompareRn = pattern.MustParse(`(UnaryExpr "!" (CallExpr (SelectorExpr (Ident "bytes") (Ident "Equal")) args))`)
)
func CheckBytesCompare(pass *analysis.Pass) (any, error) {
if pass.Pkg.Path() == "bytes" || pass.Pkg.Path() == "bytes_test" {
// the bytes package is free to use bytes.Compare as it sees fit
return nil, nil
}
for node, m := range code.Matches(pass, checkBytesCompareQ) {
args := report.RenderArgs(pass, m.State["args"].([]ast.Expr))
prefix := ""
if m.State["op"].(token.Token) == token.NEQ {
prefix = "!"
}
var fix analysis.SuggestedFix
switch tok := m.State["op"].(token.Token); tok {
case token.EQL:
fix = edit.Fix("Simplify use of bytes.Compare", edit.ReplaceWithPattern(pass.Fset, node, checkBytesCompareRe, m.State))
case token.NEQ:
fix = edit.Fix("Simplify use of bytes.Compare", edit.ReplaceWithPattern(pass.Fset, node, checkBytesCompareRn, m.State))
default:
panic(fmt.Sprintf("unexpected token %v", tok))
}
report.Report(pass, node, fmt.Sprintf("should use %sbytes.Equal(%s) instead", prefix, args), report.FilterGenerated(), report.Fixes(fix))
}
return nil, nil
}

View File

@@ -0,0 +1,108 @@
package s1005
import (
"go/ast"
"go/types"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/ast/astutil"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1005",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer, generated.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Drop unnecessary use of the blank identifier`,
Text: `In many cases, assigning to the blank identifier is unnecessary.`,
Before: `
for _ = range s {}
x, _ = someMap[key]
_ = <-ch`,
After: `
for range s{}
x = someMap[key]
<-ch`,
Since: "2017.1",
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var (
checkUnnecessaryBlankQ1 = pattern.MustParse(`
(AssignStmt
[_ (Ident "_")]
_
(Or
(IndexExpr _ _)
(UnaryExpr "<-" _))) `)
checkUnnecessaryBlankQ2 = pattern.MustParse(`
(AssignStmt
(Ident "_") _ recv@(UnaryExpr "<-" _))`)
)
func run(pass *analysis.Pass) (any, error) {
fn1 := func(node ast.Node) {
if _, ok := code.Match(pass, checkUnnecessaryBlankQ1, node); ok {
r := *node.(*ast.AssignStmt)
r.Lhs = r.Lhs[0:1]
report.Report(pass, node, "unnecessary assignment to the blank identifier",
report.FilterGenerated(),
report.Fixes(edit.Fix("Remove assignment to blank identifier", edit.ReplaceWithNode(pass.Fset, node, &r))))
} else if m, ok := code.Match(pass, checkUnnecessaryBlankQ2, node); ok {
report.Report(pass, node, "unnecessary assignment to the blank identifier",
report.FilterGenerated(),
report.Fixes(edit.Fix("Simplify channel receive operation", edit.ReplaceWithNode(pass.Fset, node, m.State["recv"].(ast.Node)))))
}
}
fn3 := func(node ast.Node) {
rs := node.(*ast.RangeStmt)
if _, ok := pass.TypesInfo.TypeOf(rs.X).Underlying().(*types.Signature); ok {
// iteration variables are not optional with rangefunc
return
}
// for _
if rs.Value == nil && astutil.IsBlank(rs.Key) {
report.Report(pass, rs.Key, "unnecessary assignment to the blank identifier",
report.FilterGenerated(),
report.MinimumLanguageVersion("go1.4"),
report.Fixes(edit.Fix("Remove assignment to blank identifier", edit.Delete(edit.Range{rs.Key.Pos(), rs.TokPos + 1}))))
}
// for _, _
if astutil.IsBlank(rs.Key) && astutil.IsBlank(rs.Value) {
// FIXME we should mark both key and value
report.Report(pass, rs.Key, "unnecessary assignment to the blank identifier",
report.FilterGenerated(),
report.MinimumLanguageVersion("go1.4"),
report.Fixes(edit.Fix("Remove assignment to blank identifier", edit.Delete(edit.Range{rs.Key.Pos(), rs.TokPos + 1}))))
}
// for x, _
if !astutil.IsBlank(rs.Key) && astutil.IsBlank(rs.Value) {
report.Report(pass, rs.Value, "unnecessary assignment to the blank identifier",
report.FilterGenerated(),
report.MinimumLanguageVersion("go1.4"),
report.Fixes(edit.Fix("Remove assignment to blank identifier", edit.Delete(edit.Range{rs.Key.End(), rs.Value.End()}))))
}
}
code.Preorder(pass, fn1, (*ast.AssignStmt)(nil))
code.Preorder(pass, fn3, (*ast.RangeStmt)(nil))
return nil, nil
}

View File

@@ -0,0 +1,46 @@
package s1006
import (
"go/ast"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1006",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer, generated.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Use \"for { ... }\" for infinite loops`,
Text: `For infinite loops, using \'for { ... }\' is the most idiomatic choice.`,
Since: "2017.1",
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
fn := func(node ast.Node) {
loop := node.(*ast.ForStmt)
if loop.Init != nil || loop.Post != nil {
return
}
if !code.IsBoolConst(pass, loop.Cond) || !code.BoolConst(pass, loop.Cond) {
return
}
report.Report(pass, loop, "should use for {} instead of for true {}",
report.ShortRange(),
report.FilterGenerated())
}
code.Preorder(pass, fn, (*ast.ForStmt)(nil))
return nil, nil
}

View File

@@ -0,0 +1,78 @@
package s1007
import (
"fmt"
"go/ast"
"strings"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1007",
Run: run,
Requires: append([]*analysis.Analyzer{generated.Analyzer}, code.RequiredAnalyzers...),
},
Doc: &lint.RawDocumentation{
Title: `Simplify regular expression by using raw string literal`,
Text: `Raw string literals use backticks instead of quotation marks and do not support
any escape sequences. This means that the backslash can be used
freely, without the need of escaping.
Since regular expressions have their own escape sequences, raw strings
can improve their readability.`,
Before: `regexp.Compile("\\A(\\w+) profile: total \\d+\\n\\z")`,
After: "regexp.Compile(`\\A(\\w+) profile: total \\d+\\n\\z`)",
Since: "2017.1",
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
// TODO(dominikh): support string concat, maybe support constants
var query = pattern.MustParse(`(CallExpr (Symbol fn@(Or "regexp.MustCompile" "regexp.Compile")) [lit@(BasicLit "STRING" _)])`)
func run(pass *analysis.Pass) (any, error) {
outer:
for _, m := range code.Matches(pass, query) {
lit := m.State["lit"].(*ast.BasicLit)
val := lit.Value
if lit.Value[0] != '"' {
// already a raw string
continue
}
if !strings.Contains(val, `\\`) {
continue
}
if strings.Contains(val, "`") {
continue
}
bs := false
for _, c := range val {
if !bs && c == '\\' {
bs = true
continue
}
if bs && c == '\\' {
bs = false
continue
}
if bs {
// backslash followed by non-backslash -> escape sequence
continue outer
}
}
report.Report(pass, lit, fmt.Sprintf("should use raw string (`...`) with %s to avoid having to escape twice", m.State["fn"]), report.FilterGenerated())
}
return nil, nil
}

View File

@@ -0,0 +1,177 @@
package s1008
import (
"fmt"
"go/ast"
"go/constant"
"go/token"
"strings"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1008",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer, generated.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Simplify returning boolean expression`,
Before: `
if <expr> {
return true
}
return false`,
After: `return <expr>`,
Since: "2017.1",
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var (
checkIfReturnQIf = pattern.MustParse(`(IfStmt nil cond [(ReturnStmt [ret@(Builtin (Or "true" "false"))])] nil)`)
checkIfReturnQRet = pattern.MustParse(`(ReturnStmt [ret@(Builtin (Or "true" "false"))])`)
)
func run(pass *analysis.Pass) (any, error) {
var cm ast.CommentMap
fn := func(node ast.Node) {
if f, ok := node.(*ast.File); ok {
cm = ast.NewCommentMap(pass.Fset, f, f.Comments)
return
}
block := node.(*ast.BlockStmt)
l := len(block.List)
if l < 2 {
return
}
n1, n2 := block.List[l-2], block.List[l-1]
if len(block.List) >= 3 {
if _, ok := block.List[l-3].(*ast.IfStmt); ok {
// Do not flag a series of if statements
return
}
}
m1, ok := code.Match(pass, checkIfReturnQIf, n1)
if !ok {
return
}
m2, ok := code.Match(pass, checkIfReturnQRet, n2)
if !ok {
return
}
if op, ok := m1.State["cond"].(*ast.BinaryExpr); ok {
switch op.Op {
case token.EQL, token.LSS, token.GTR, token.NEQ, token.LEQ, token.GEQ:
default:
return
}
}
ret1 := m1.State["ret"].(*ast.Ident)
ret2 := m2.State["ret"].(*ast.Ident)
if ret1.Name == ret2.Name {
// we want the function to return true and false, not the
// same value both times.
return
}
hasComments := func(n ast.Node) bool {
cmf := cm.Filter(n)
for _, groups := range cmf {
for _, group := range groups {
for _, cmt := range group.List {
if strings.HasPrefix(cmt.Text, "//@ diag") {
// Staticcheck test cases use comments to mark
// expected diagnostics. Ignore these comments so we
// can test this check.
continue
}
return true
}
}
}
return false
}
// Don't flag if either branch is commented
if hasComments(n1) || hasComments(n2) {
return
}
cond := m1.State["cond"].(ast.Expr)
origCond := cond
if ret1.Name == "false" {
cond = negate(pass, cond)
}
report.Report(pass, n1,
fmt.Sprintf("should use 'return %s' instead of 'if %s { return %s }; return %s'",
report.Render(pass, cond),
report.Render(pass, origCond), report.Render(pass, ret1), report.Render(pass, ret2)),
report.FilterGenerated())
}
code.Preorder(pass, fn, (*ast.File)(nil), (*ast.BlockStmt)(nil))
return nil, nil
}
func negate(pass *analysis.Pass, expr ast.Expr) ast.Expr {
switch expr := expr.(type) {
case *ast.BinaryExpr:
out := *expr
switch expr.Op {
case token.EQL:
out.Op = token.NEQ
case token.LSS:
out.Op = token.GEQ
case token.GTR:
// Some builtins never return negative ints; "len(x) <= 0" should be "len(x) == 0".
if call, ok := expr.X.(*ast.CallExpr); ok &&
code.IsCallToAny(pass, call, "len", "cap", "copy") &&
code.IsIntegerLiteral(pass, expr.Y, constant.MakeInt64(0)) {
out.Op = token.EQL
} else {
out.Op = token.LEQ
}
case token.NEQ:
out.Op = token.EQL
case token.LEQ:
out.Op = token.GTR
case token.GEQ:
out.Op = token.LSS
}
return &out
case *ast.Ident, *ast.CallExpr, *ast.IndexExpr, *ast.StarExpr:
return &ast.UnaryExpr{
Op: token.NOT,
X: expr,
}
case *ast.UnaryExpr:
if expr.Op == token.NOT {
return expr.X
}
return &ast.UnaryExpr{
Op: token.NOT,
X: expr,
}
default:
return &ast.UnaryExpr{
Op: token.NOT,
X: &ast.ParenExpr{
X: expr,
},
}
}
}

View File

@@ -0,0 +1,180 @@
package s1009
import (
"fmt"
"go/ast"
"go/constant"
"go/token"
"go/types"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1009",
Run: run,
Requires: append([]*analysis.Analyzer{generated.Analyzer}, code.RequiredAnalyzers...),
},
Doc: &lint.RawDocumentation{
Title: `Omit redundant nil check on slices, maps, and channels`,
Text: `The \'len\' function is defined for all slices, maps, and
channels, even nil ones, which have a length of zero. It is not necessary to
check for nil before checking that their length is not zero.`,
Before: `if x != nil && len(x) != 0 {}`,
After: `if len(x) != 0 {}`,
Since: "2017.1",
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var query = pattern.MustParse(`
(BinaryExpr
(BinaryExpr
x
lhsOp@(Or "==" "!=")
nilly)
outerOp@(Or "&&" "||")
(BinaryExpr
(CallExpr (Builtin "len") [x])
rhsOp
k))`)
// run checks for the following redundant nil-checks:
//
// if x == nil || len(x) == 0 {}
// if x == nil || len(x) < N {} (where N != 0)
// if x == nil || len(x) <= N {}
// if x != nil && len(x) != 0 {}
// if x != nil && len(x) == N {} (where N != 0)
// if x != nil && len(x) > N {}
// if x != nil && len(x) >= N {} (where N != 0)
func run(pass *analysis.Pass) (any, error) {
isConstZero := func(expr ast.Expr) (isConst bool, isZero bool) {
_, ok := expr.(*ast.BasicLit)
if ok {
return true, code.IsIntegerLiteral(pass, expr, constant.MakeInt64(0))
}
id, ok := expr.(*ast.Ident)
if !ok {
return false, false
}
c, ok := pass.TypesInfo.ObjectOf(id).(*types.Const)
if !ok {
return false, false
}
return true, c.Val().Kind() == constant.Int && c.Val().String() == "0"
}
for node, m := range code.Matches(pass, query) {
x := m.State["x"].(ast.Expr)
outerOp := m.State["outerOp"].(token.Token)
lhsOp := m.State["lhsOp"].(token.Token)
rhsOp := m.State["rhsOp"].(token.Token)
nilly := m.State["nilly"].(ast.Expr)
k := m.State["k"].(ast.Expr)
eqNil := outerOp == token.LOR
if code.MayHaveSideEffects(pass, x, nil) {
continue
}
if eqNil && lhsOp != token.EQL {
continue
}
if !eqNil && lhsOp != token.NEQ {
continue
}
if !code.IsNil(pass, nilly) {
continue
}
isConst, isZero := isConstZero(k)
if !isConst {
continue
}
if eqNil {
switch rhsOp {
case token.EQL:
// avoid false positive for "xx == nil || len(xx) == <non-zero>"
if !isZero {
continue
}
case token.LEQ:
// ok
case token.LSS:
// avoid false positive for "xx == nil || len(xx) < 0"
if isZero {
continue
}
default:
continue
}
} else {
switch rhsOp {
case token.EQL:
// avoid false positive for "xx != nil && len(xx) == 0"
if isZero {
continue
}
case token.GEQ:
// avoid false positive for "xx != nil && len(xx) >= 0"
if isZero {
continue
}
case token.NEQ:
// avoid false positive for "xx != nil && len(xx) != <non-zero>"
if !isZero {
continue
}
case token.GTR:
// ok
default:
continue
}
}
// finally check that xx type is one of array, slice, map or chan
// this is to prevent false positive in case if xx is a pointer to an array
typ := pass.TypesInfo.TypeOf(x)
var nilType string
ok := typeutil.All(typ, func(term *types.Term) bool {
switch term.Type().Underlying().(type) {
case *types.Slice:
nilType = "nil slices"
return true
case *types.Map:
nilType = "nil maps"
return true
case *types.Chan:
nilType = "nil channels"
return true
case *types.Pointer:
return false
case *types.TypeParam:
return false
default:
lint.ExhaustiveTypeSwitch(term.Type().Underlying())
return false
}
})
if !ok {
continue
}
report.Report(pass, node,
fmt.Sprintf("should omit nil check; len() for %s is defined as zero", nilType),
report.FilterGenerated())
}
return nil, nil
}

View File

@@ -0,0 +1,44 @@
package s1010
import (
"go/ast"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1010",
Run: run,
Requires: append([]*analysis.Analyzer{generated.Analyzer}, code.RequiredAnalyzers...),
},
Doc: &lint.RawDocumentation{
Title: `Omit default slice index`,
Text: `When slicing, the second index defaults to the length of the value,
making \'s[n:len(s)]\' and \'s[n:]\' equivalent.`,
Since: "2017.1",
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var checkSlicingQ = pattern.MustParse(`(SliceExpr x@(Object _) low (CallExpr (Builtin "len") [x]) nil)`)
func run(pass *analysis.Pass) (any, error) {
for node := range code.Matches(pass, checkSlicingQ) {
expr := node.(*ast.SliceExpr)
report.Report(pass, expr.High,
"should omit second index in slice, s[a:len(s)] is identical to s[a:]",
report.FilterGenerated(),
report.Fixes(edit.Fix("Simplify slice expression", edit.Delete(expr.High))))
}
return nil, nil
}

View File

@@ -0,0 +1,137 @@
package s1011
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/facts/purity"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1011",
Run: run,
Requires: append([]*analysis.Analyzer{generated.Analyzer, purity.Analyzer}, code.RequiredAnalyzers...),
},
Doc: &lint.RawDocumentation{
Title: `Use a single \'append\' to concatenate two slices`,
Before: `
for _, e := range y {
x = append(x, e)
}
for i := range y {
x = append(x, y[i])
}
for i := range y {
v := y[i]
x = append(x, v)
}`,
After: `
x = append(x, y...)
x = append(x, y...)
x = append(x, y...)`,
Since: "2017.1",
// MergeIfAll because y might not be a slice under all build tags.
MergeIf: lint.MergeIfAll,
},
})
var Analyzer = SCAnalyzer.Analyzer
var checkLoopAppendQ = pattern.MustParse(`
(Or
(RangeStmt
(Ident "_")
val@(Object _)
_
x
[(AssignStmt [lhs] "=" [(CallExpr (Builtin "append") [lhs val])])])
(RangeStmt
idx@(Object _)
nil
_
x
[(AssignStmt [lhs] "=" [(CallExpr (Builtin "append") [lhs (IndexExpr x idx)])])])
(RangeStmt
idx@(Object _)
nil
_
x
[(AssignStmt val@(Object _) ":=" (IndexExpr x idx))
(AssignStmt [lhs] "=" [(CallExpr (Builtin "append") [lhs val])])]))`)
func run(pass *analysis.Pass) (any, error) {
pure := pass.ResultOf[purity.Analyzer].(purity.Result)
for node, m := range code.Matches(pass, checkLoopAppendQ) {
if val, ok := m.State["val"].(types.Object); ok && code.RefersTo(pass, m.State["lhs"].(ast.Expr), val) {
continue
}
if m.State["idx"] != nil && code.MayHaveSideEffects(pass, m.State["x"].(ast.Expr), pure) {
// When using an index-based loop, x gets evaluated repeatedly and thus should be pure.
// This doesn't matter for value-based loops, because x only gets evaluated once.
continue
}
if idx, ok := m.State["idx"].(types.Object); ok && code.RefersTo(pass, m.State["lhs"].(ast.Expr), idx) {
// The lhs mustn't refer to the index loop variable.
continue
}
if code.MayHaveSideEffects(pass, m.State["lhs"].(ast.Expr), pure) {
// The lhs may be dynamic and return different values on each iteration. For example:
//
// func bar() map[int][]int { /* return one of several maps */ }
//
// func foo(x []int, y [][]int) {
// for i := range x {
// bar()[0] = append(bar()[0], x[i])
// }
// }
//
// The dynamic nature of the lhs might also affect the value of the index.
continue
}
src := pass.TypesInfo.TypeOf(m.State["x"].(ast.Expr))
dst := pass.TypesInfo.TypeOf(m.State["lhs"].(ast.Expr))
if !types.Identical(src, dst) {
continue
}
r := &ast.AssignStmt{
Lhs: []ast.Expr{m.State["lhs"].(ast.Expr)},
Tok: token.ASSIGN,
Rhs: []ast.Expr{
&ast.CallExpr{
Fun: &ast.Ident{Name: "append"},
Args: []ast.Expr{
m.State["lhs"].(ast.Expr),
m.State["x"].(ast.Expr),
},
Ellipsis: 1,
},
},
}
report.Report(pass, node, fmt.Sprintf("should replace loop with %s", report.Render(pass, r)),
report.ShortRange(),
report.FilterGenerated(),
report.Fixes(edit.Fix("Replace loop with call to append", edit.ReplaceWithNode(pass.Fset, node, r))))
}
return nil, nil
}

View File

@@ -0,0 +1,46 @@
package s1012
import (
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1012",
Run: run,
Requires: append([]*analysis.Analyzer{generated.Analyzer}, code.RequiredAnalyzers...),
},
Doc: &lint.RawDocumentation{
Title: `Replace \'time.Now().Sub(x)\' with \'time.Since(x)\'`,
Text: `The \'time.Since\' helper has the same effect as using \'time.Now().Sub(x)\'
but is easier to read.`,
Before: `time.Now().Sub(x)`,
After: `time.Since(x)`,
Since: "2017.1",
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var (
checkTimeSinceQ = pattern.MustParse(`(CallExpr (SelectorExpr (CallExpr (Symbol "time.Now") []) (Symbol "(time.Time).Sub")) [arg])`)
checkTimeSinceR = pattern.MustParse(`(CallExpr (SelectorExpr (Ident "time") (Ident "Since")) [arg])`)
)
func run(pass *analysis.Pass) (any, error) {
for node, m := range code.Matches(pass, checkTimeSinceQ) {
edits := code.EditMatch(pass, node, m, checkTimeSinceR)
report.Report(pass, node, "should use time.Since instead of time.Now().Sub",
report.FilterGenerated(),
report.Fixes(edit.Fix("Replace with call to time.Since", edits...)))
}
return nil, nil
}

View File

@@ -0,0 +1,193 @@
package s1016
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"go/version"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1016",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer, generated.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Use a type conversion instead of manually copying struct fields`,
Text: `Two struct types with identical fields can be converted between each
other. In older versions of Go, the fields had to have identical
struct tags. Since Go 1.8, however, struct tags are ignored during
conversions. It is thus not necessary to manually copy every field
individually.`,
Before: `
var x T1
y := T2{
Field1: x.Field1,
Field2: x.Field2,
}`,
After: `
var x T1
y := T2(x)`,
Since: "2017.1",
MergeIf: lint.MergeIfAll,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
// TODO(dh): support conversions between type parameters
fn := func(c inspector.Cursor) {
node := c.Node()
if unary, ok := c.Parent().Node().(*ast.UnaryExpr); ok && unary.Op == token.AND {
// Do not suggest type conversion between pointers
return
}
lit := node.(*ast.CompositeLit)
var typ1 types.Type
var named1 *types.Named
switch typ := pass.TypesInfo.TypeOf(lit.Type).(type) {
case *types.Named:
typ1 = typ
named1 = typ
case *types.Alias:
ua := types.Unalias(typ)
if n, ok := ua.(*types.Named); ok {
typ1 = typ
named1 = n
}
}
if typ1 == nil {
return
}
s1, ok := typ1.Underlying().(*types.Struct)
if !ok {
return
}
var typ2 types.Type
var named2 *types.Named
var ident *ast.Ident
getSelType := func(expr ast.Expr) (types.Type, *ast.Ident, bool) {
sel, ok := expr.(*ast.SelectorExpr)
if !ok {
return nil, nil, false
}
ident, ok := sel.X.(*ast.Ident)
if !ok {
return nil, nil, false
}
typ := pass.TypesInfo.TypeOf(sel.X)
return typ, ident, typ != nil
}
if len(lit.Elts) == 0 {
return
}
if s1.NumFields() != len(lit.Elts) {
return
}
for i, elt := range lit.Elts {
var t types.Type
var id *ast.Ident
var ok bool
switch elt := elt.(type) {
case *ast.SelectorExpr:
t, id, ok = getSelType(elt)
if !ok {
return
}
if i >= s1.NumFields() || s1.Field(i).Name() != elt.Sel.Name {
return
}
case *ast.KeyValueExpr:
var sel *ast.SelectorExpr
sel, ok = elt.Value.(*ast.SelectorExpr)
if !ok {
return
}
if elt.Key.(*ast.Ident).Name != sel.Sel.Name {
return
}
t, id, ok = getSelType(elt.Value)
}
if !ok {
return
}
// All fields must be initialized from the same object
if ident != nil && pass.TypesInfo.ObjectOf(ident) != pass.TypesInfo.ObjectOf(id) {
return
}
switch t := t.(type) {
case *types.Named:
typ2 = t
named2 = t
case *types.Alias:
if n, ok := types.Unalias(t).(*types.Named); ok {
typ2 = t
named2 = n
}
}
if typ2 == nil {
return
}
ident = id
}
if typ2 == nil {
return
}
if named1.Obj().Pkg() != named2.Obj().Pkg() {
// Do not suggest type conversions between different
// packages. Types in different packages might only match
// by coincidence. Furthermore, if the dependency ever
// adds more fields to its type, it could break the code
// that relies on the type conversion to work.
return
}
s2, ok := typ2.Underlying().(*types.Struct)
if !ok {
return
}
if typ1 == typ2 {
return
}
if version.Compare(code.LanguageVersion(pass, node), "go1.8") >= 0 {
if !types.IdenticalIgnoreTags(s1, s2) {
return
}
} else {
if !types.Identical(s1, s2) {
return
}
}
r := &ast.CallExpr{
Fun: lit.Type,
Args: []ast.Expr{ident},
}
report.Report(pass, node,
fmt.Sprintf("should convert %s (type %s) to %s instead of using struct literal", ident.Name, types.TypeString(typ2, types.RelativeTo(pass.Pkg)), types.TypeString(typ1, types.RelativeTo(pass.Pkg))),
report.FilterGenerated(),
report.Fixes(edit.Fix("Use type conversion", edit.ReplaceWithNode(pass.Fset, node, r))))
}
for c := range code.Cursor(pass).Preorder((*ast.CompositeLit)(nil)) {
fn(c)
}
return nil, nil
}

View File

@@ -0,0 +1,239 @@
package s1017
import (
"fmt"
"go/ast"
"go/token"
"reflect"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/ast/astutil"
"honnef.co/go/tools/knowledge"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1017",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer, generated.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Replace manual trimming with \'strings.TrimPrefix\'`,
Text: `Instead of using \'strings.HasPrefix\' and manual slicing, use the
\'strings.TrimPrefix\' function. If the string doesn't start with the
prefix, the original string will be returned. Using \'strings.TrimPrefix\'
reduces complexity, and avoids common bugs, such as off-by-one
mistakes.`,
Before: `
if strings.HasPrefix(str, prefix) {
str = str[len(prefix):]
}`,
After: `str = strings.TrimPrefix(str, prefix)`,
Since: "2017.1",
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
sameNonDynamic := func(node1, node2 ast.Node) bool {
if reflect.TypeOf(node1) != reflect.TypeOf(node2) {
return false
}
switch node1 := node1.(type) {
case *ast.Ident:
return pass.TypesInfo.ObjectOf(node1) == pass.TypesInfo.ObjectOf(node2.(*ast.Ident))
case *ast.SelectorExpr, *ast.IndexExpr:
return astutil.Equal(node1, node2)
case *ast.BasicLit:
return astutil.Equal(node1, node2)
}
return false
}
isLenOnIdent := func(fn ast.Expr, ident ast.Expr) bool {
call, ok := fn.(*ast.CallExpr)
if !ok {
return false
}
if !code.IsCallTo(pass, call, "len") {
return false
}
if len(call.Args) != 1 {
return false
}
return sameNonDynamic(call.Args[knowledge.Arg("len.v")], ident)
}
seen := make(map[ast.Node]struct{})
fn := func(node ast.Node) {
var pkg string
var fun string
ifstmt := node.(*ast.IfStmt)
if ifstmt.Init != nil {
return
}
if ifstmt.Else != nil {
seen[ifstmt.Else] = struct{}{}
return
}
if _, ok := seen[ifstmt]; ok {
return
}
if len(ifstmt.Body.List) != 1 {
return
}
condCall, ok := ifstmt.Cond.(*ast.CallExpr)
if !ok {
return
}
condCallName := code.CallName(pass, condCall)
switch condCallName {
case "strings.HasPrefix":
pkg = "strings"
fun = "HasPrefix"
case "strings.HasSuffix":
pkg = "strings"
fun = "HasSuffix"
case "strings.Contains":
pkg = "strings"
fun = "Contains"
case "bytes.HasPrefix":
pkg = "bytes"
fun = "HasPrefix"
case "bytes.HasSuffix":
pkg = "bytes"
fun = "HasSuffix"
case "bytes.Contains":
pkg = "bytes"
fun = "Contains"
default:
return
}
assign, ok := ifstmt.Body.List[0].(*ast.AssignStmt)
if !ok {
return
}
if assign.Tok != token.ASSIGN {
return
}
if len(assign.Lhs) != 1 || len(assign.Rhs) != 1 {
return
}
if !sameNonDynamic(condCall.Args[0], assign.Lhs[0]) {
return
}
switch rhs := assign.Rhs[0].(type) {
case *ast.CallExpr:
if len(rhs.Args) < 2 || !sameNonDynamic(condCall.Args[0], rhs.Args[0]) || !sameNonDynamic(condCall.Args[1], rhs.Args[1]) {
return
}
rhsName := code.CallName(pass, rhs)
if condCallName == "strings.HasPrefix" && rhsName == "strings.TrimPrefix" ||
condCallName == "strings.HasSuffix" && rhsName == "strings.TrimSuffix" ||
condCallName == "strings.Contains" && rhsName == "strings.Replace" ||
condCallName == "bytes.HasPrefix" && rhsName == "bytes.TrimPrefix" ||
condCallName == "bytes.HasSuffix" && rhsName == "bytes.TrimSuffix" ||
condCallName == "bytes.Contains" && rhsName == "bytes.Replace" {
report.Report(pass, ifstmt, fmt.Sprintf("should replace this if statement with an unconditional %s", rhsName), report.FilterGenerated())
}
case *ast.SliceExpr:
slice := rhs
if !ok {
return
}
if slice.Slice3 {
return
}
if !sameNonDynamic(slice.X, condCall.Args[0]) {
return
}
validateOffset := func(off ast.Expr) bool {
switch off := off.(type) {
case *ast.CallExpr:
return isLenOnIdent(off, condCall.Args[1])
case *ast.BasicLit:
if pkg != "strings" {
return false
}
if _, ok := condCall.Args[1].(*ast.BasicLit); !ok {
// Only allow manual slicing with an integer
// literal if the second argument to HasPrefix
// was a string literal.
return false
}
s, ok1 := code.ExprToString(pass, condCall.Args[1])
n, ok2 := code.ExprToInt(pass, off)
if !ok1 || !ok2 || n != int64(len(s)) {
return false
}
return true
default:
return false
}
}
switch fun {
case "HasPrefix":
// TODO(dh) We could detect a High that is len(s), but another
// rule will already flag that, anyway.
if slice.High != nil {
return
}
if !validateOffset(slice.Low) {
return
}
case "HasSuffix":
if slice.Low != nil {
n, ok := code.ExprToInt(pass, slice.Low)
if !ok || n != 0 {
return
}
}
switch index := slice.High.(type) {
case *ast.BinaryExpr:
if index.Op != token.SUB {
return
}
if !isLenOnIdent(index.X, condCall.Args[0]) {
return
}
if !validateOffset(index.Y) {
return
}
default:
return
}
default:
return
}
var replacement string
switch fun {
case "HasPrefix":
replacement = "TrimPrefix"
case "HasSuffix":
replacement = "TrimSuffix"
}
report.Report(pass, ifstmt, fmt.Sprintf("should replace this if statement with an unconditional %s.%s", pkg, replacement),
report.ShortRange(),
report.FilterGenerated())
}
}
code.Preorder(pass, fn, (*ast.IfStmt)(nil))
return nil, nil
}

View File

@@ -0,0 +1,78 @@
package s1018
import (
"go/ast"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1018",
Run: run,
Requires: append([]*analysis.Analyzer{generated.Analyzer}, code.RequiredAnalyzers...),
},
Doc: &lint.RawDocumentation{
Title: `Use \"copy\" for sliding elements`,
Text: `\'copy()\' permits using the same source and destination slice, even with
overlapping ranges. This makes it ideal for sliding elements in a
slice.`,
Before: `
for i := 0; i < n; i++ {
bs[i] = bs[offset+i]
}`,
After: `copy(bs[:n], bs[offset:])`,
Since: "2017.1",
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var (
checkLoopSlideQ = pattern.MustParse(`
(ForStmt
(AssignStmt initvar@(Ident _) _ (IntegerLiteral "0"))
(BinaryExpr initvar "<" limit@(Ident _))
(IncDecStmt initvar "++")
[(AssignStmt
(IndexExpr slice@(Ident _) initvar)
"="
(IndexExpr slice (BinaryExpr offset@(Ident _) "+" initvar)))])`)
checkLoopSlideR = pattern.MustParse(`
(CallExpr
(Ident "copy")
[(SliceExpr slice nil limit nil)
(SliceExpr slice offset nil nil)])`)
)
func run(pass *analysis.Pass) (any, error) {
// TODO(dh): detect bs[i+offset] in addition to bs[offset+i]
// TODO(dh): consider merging this function with LintLoopCopy
// TODO(dh): detect length that is an expression, not a variable name
// TODO(dh): support sliding to a different offset than the beginning of the slice
for node, m := range code.Matches(pass, checkLoopSlideQ) {
typ := pass.TypesInfo.TypeOf(m.State["slice"].(*ast.Ident))
// The pattern probably needs a core type, but All is fine, too. Either way we only accept slices.
if !typeutil.All(typ, typeutil.IsSlice) {
continue
}
edits := code.EditMatch(pass, node, m, checkLoopSlideR)
report.Report(pass, node, "should use copy() instead of loop for sliding slice elements",
report.ShortRange(),
report.FilterGenerated(),
report.Fixes(edit.Fix("Use copy() instead of loop", edits...)))
}
return nil, nil
}

View File

@@ -0,0 +1,71 @@
package s1019
import (
"fmt"
"go/ast"
"go/types"
"path/filepath"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1019",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer, generated.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Simplify \"make\" call by omitting redundant arguments`,
Text: `The \"make\" function has default values for the length and capacity
arguments. For channels, the length defaults to zero, and for slices,
the capacity defaults to the length.`,
Since: "2017.1",
// MergeIfAll because the type might be different under different build tags.
// You shouldn't write code like that…
MergeIf: lint.MergeIfAll,
},
})
var Analyzer = SCAnalyzer.Analyzer
var (
checkMakeLenCapQ1 = pattern.MustParse(`(CallExpr (Builtin "make") [typ size@(IntegerLiteral "0")])`)
checkMakeLenCapQ2 = pattern.MustParse(`(CallExpr (Builtin "make") [typ size size])`)
)
func run(pass *analysis.Pass) (any, error) {
fn := func(node ast.Node) {
if pass.Pkg.Path() == "runtime_test" && filepath.Base(pass.Fset.Position(node.Pos()).Filename) == "map_test.go" {
// special case of runtime tests testing map creation
return
}
if m, ok := code.Match(pass, checkMakeLenCapQ1, node); ok {
T := m.State["typ"].(ast.Expr)
size := m.State["size"].(ast.Node)
if _, ok := typeutil.CoreType(pass.TypesInfo.TypeOf(T)).Underlying().(*types.Chan); ok {
report.Report(pass, size, fmt.Sprintf("should use make(%s) instead", report.Render(pass, T)), report.FilterGenerated())
}
} else if m, ok := code.Match(pass, checkMakeLenCapQ2, node); ok {
// TODO(dh): don't consider sizes identical if they're
// dynamic. for example: make(T, <-ch, <-ch).
T := m.State["typ"].(ast.Expr)
size := m.State["size"].(ast.Node)
report.Report(pass, size,
fmt.Sprintf("should use make(%s, %s) instead", report.Render(pass, T), report.Render(pass, size)),
report.FilterGenerated())
}
}
code.Preorder(pass, fn, (*ast.CallExpr)(nil))
return nil, nil
}

View File

@@ -0,0 +1,74 @@
package s1020
import (
"fmt"
"go/ast"
"go/types"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1020",
Run: run,
Requires: append([]*analysis.Analyzer{generated.Analyzer}, code.RequiredAnalyzers...),
},
Doc: &lint.RawDocumentation{
Title: `Omit redundant nil check in type assertion`,
Before: `if _, ok := i.(T); ok && i != nil {}`,
After: `if _, ok := i.(T); ok {}`,
Since: "2017.1",
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var (
checkAssertNotNilFn1Q = pattern.MustParse(`
(IfStmt
(AssignStmt [(Ident "_") ok@(Object _)] _ [(TypeAssertExpr assert@(Object _) _)])
(Or
(BinaryExpr ok "&&" (BinaryExpr assert "!=" (Builtin "nil")))
(BinaryExpr (BinaryExpr assert "!=" (Builtin "nil")) "&&" ok))
_
_)`)
checkAssertNotNilFn2Q = pattern.MustParse(`
(IfStmt
nil
(BinaryExpr lhs@(Object _) "!=" (Builtin "nil"))
[
ifstmt@(IfStmt
(AssignStmt [(Ident "_") ok@(Object _)] _ [(TypeAssertExpr lhs _)])
ok
_
nil)
]
nil)`)
)
func run(pass *analysis.Pass) (any, error) {
for node, m := range code.Matches(pass, checkAssertNotNilFn1Q) {
assert := m.State["assert"].(types.Object)
assign := m.State["ok"].(types.Object)
report.Report(pass, node, fmt.Sprintf("when %s is true, %s can't be nil", assign.Name(), assert.Name()),
report.ShortRange(),
report.FilterGenerated())
}
for _, m := range code.Matches(pass, checkAssertNotNilFn2Q) {
ifstmt := m.State["ifstmt"].(*ast.IfStmt)
lhs := m.State["lhs"].(types.Object)
assignIdent := m.State["ok"].(types.Object)
report.Report(pass, ifstmt, fmt.Sprintf("when %s is true, %s can't be nil", assignIdent.Name(), lhs.Name()),
report.ShortRange(),
report.FilterGenerated())
}
return nil, nil
}

View File

@@ -0,0 +1,118 @@
package s1021
import (
"go/ast"
"go/token"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1021",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer, generated.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Merge variable declaration and assignment`,
Before: `
var x uint
x = 1`,
After: `var x uint = 1`,
Since: "2017.1",
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
hasMultipleAssignments := func(root ast.Node, ident *ast.Ident) bool {
num := 0
ast.Inspect(root, func(node ast.Node) bool {
if num >= 2 {
return false
}
assign, ok := node.(*ast.AssignStmt)
if !ok {
return true
}
for _, lhs := range assign.Lhs {
if oident, ok := lhs.(*ast.Ident); ok {
if pass.TypesInfo.ObjectOf(oident) == pass.TypesInfo.ObjectOf(ident) {
num++
}
}
}
return true
})
return num >= 2
}
fn := func(node ast.Node) {
block := node.(*ast.BlockStmt)
if len(block.List) < 2 {
return
}
for i, stmt := range block.List[:len(block.List)-1] {
_ = i
decl, ok := stmt.(*ast.DeclStmt)
if !ok {
continue
}
gdecl, ok := decl.Decl.(*ast.GenDecl)
if !ok || gdecl.Tok != token.VAR || len(gdecl.Specs) != 1 {
continue
}
vspec, ok := gdecl.Specs[0].(*ast.ValueSpec)
if !ok || len(vspec.Names) != 1 || len(vspec.Values) != 0 {
continue
}
assign, ok := block.List[i+1].(*ast.AssignStmt)
if !ok || assign.Tok != token.ASSIGN {
continue
}
if len(assign.Lhs) != 1 || len(assign.Rhs) != 1 {
continue
}
ident, ok := assign.Lhs[0].(*ast.Ident)
if !ok {
continue
}
if pass.TypesInfo.ObjectOf(vspec.Names[0]) != pass.TypesInfo.ObjectOf(ident) {
continue
}
if code.RefersTo(pass, assign.Rhs[0], pass.TypesInfo.ObjectOf(ident)) {
continue
}
if hasMultipleAssignments(block, ident) {
continue
}
r := &ast.GenDecl{
Specs: []ast.Spec{
&ast.ValueSpec{
Names: vspec.Names,
Values: []ast.Expr{assign.Rhs[0]},
Type: vspec.Type,
},
},
Tok: gdecl.Tok,
}
report.Report(pass, decl, "should merge variable declaration with assignment on next line",
report.FilterGenerated(),
report.Fixes(edit.Fix("Merge declaration with assignment", edit.ReplaceWithNode(pass.Fset, edit.Range{decl.Pos(), assign.End()}, r))))
}
}
code.Preorder(pass, fn, (*ast.BlockStmt)(nil))
return nil, nil
}

View File

@@ -0,0 +1,79 @@
package s1023
import (
"go/ast"
"go/token"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1023",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer, generated.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Omit redundant control flow`,
Text: `Functions that have no return value do not need a return statement as
the final statement of the function.
Switches in Go do not have automatic fallthrough, unlike languages
like C. It is not necessary to have a break statement as the final
statement in a case block.`,
Since: "2017.1",
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
fn1 := func(node ast.Node) {
clause := node.(*ast.CaseClause)
if len(clause.Body) < 2 {
return
}
branch, ok := clause.Body[len(clause.Body)-1].(*ast.BranchStmt)
if !ok || branch.Tok != token.BREAK || branch.Label != nil {
return
}
report.Report(pass, branch, "redundant break statement", report.FilterGenerated())
}
fn2 := func(node ast.Node) {
var ret *ast.FieldList
var body *ast.BlockStmt
switch x := node.(type) {
case *ast.FuncDecl:
ret = x.Type.Results
body = x.Body
case *ast.FuncLit:
ret = x.Type.Results
body = x.Body
default:
lint.ExhaustiveTypeSwitch(node)
}
// if the func has results, a return can't be redundant.
// similarly, if there are no statements, there can be
// no return.
if ret != nil || body == nil || len(body.List) < 1 {
return
}
rst, ok := body.List[len(body.List)-1].(*ast.ReturnStmt)
if !ok {
return
}
// we don't need to check rst.Results as we already
// checked x.Type.Results to be nil.
report.Report(pass, rst, "redundant return statement", report.FilterGenerated())
}
code.Preorder(pass, fn1, (*ast.CaseClause)(nil))
code.Preorder(pass, fn2, (*ast.FuncDecl)(nil), (*ast.FuncLit)(nil))
return nil, nil
}

View File

@@ -0,0 +1,55 @@
package s1024
import (
"go/ast"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1024",
Run: run,
Requires: append([]*analysis.Analyzer{generated.Analyzer}, code.RequiredAnalyzers...),
},
Doc: &lint.RawDocumentation{
Title: `Replace \'x.Sub(time.Now())\' with \'time.Until(x)\'`,
Text: `The \'time.Until\' helper has the same effect as using \'x.Sub(time.Now())\'
but is easier to read.`,
Before: `x.Sub(time.Now())`,
After: `time.Until(x)`,
Since: "2017.1",
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var (
checkTimeUntilQ = pattern.MustParse(`(CallExpr (Symbol "(time.Time).Sub") [(CallExpr (Symbol "time.Now") [])])`)
checkTimeUntilR = pattern.MustParse(`(CallExpr (SelectorExpr (Ident "time") (Ident "Until")) [arg])`)
)
func run(pass *analysis.Pass) (any, error) {
for node := range code.Matches(pass, checkTimeUntilQ) {
if sel, ok := node.(*ast.CallExpr).Fun.(*ast.SelectorExpr); ok {
r := pattern.NodeToAST(checkTimeUntilR.Root, map[string]any{"arg": sel.X}).(ast.Node)
report.Report(pass, node, "should use time.Until instead of t.Sub(time.Now())",
report.FilterGenerated(),
report.MinimumStdlibVersion("go1.8"),
report.Fixes(edit.Fix("Replace with call to time.Until", edit.ReplaceWithNode(pass.Fset, node, r))))
} else {
report.Report(pass, node, "should use time.Until instead of t.Sub(time.Now())",
report.MinimumStdlibVersion("go1.8"),
report.FilterGenerated())
}
}
return nil, nil
}

View File

@@ -0,0 +1,154 @@
package s1025
import (
"go/ast"
"go/types"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/internal/passes/buildir"
"honnef.co/go/tools/knowledge"
"honnef.co/go/tools/pattern"
"golang.org/x/exp/typeparams"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1025",
Run: run,
Requires: append([]*analysis.Analyzer{
buildir.Analyzer,
generated.Analyzer,
}, code.RequiredAnalyzers...),
},
Doc: &lint.RawDocumentation{
Title: `Don't use \'fmt.Sprintf("%s", x)\' unnecessarily`,
Text: `In many instances, there are easier and more efficient ways of getting
a value's string representation. Whenever a value's underlying type is
a string already, or the type has a String method, they should be used
directly.
Given the following shared definitions
type T1 string
type T2 int
func (T2) String() string { return "Hello, world" }
var x string
var y T1
var z T2
we can simplify
fmt.Sprintf("%s", x)
fmt.Sprintf("%s", y)
fmt.Sprintf("%s", z)
to
x
string(y)
z.String()
`,
Since: "2017.1",
MergeIf: lint.MergeIfAll,
},
})
var Analyzer = SCAnalyzer.Analyzer
var checkRedundantSprintfQ = pattern.MustParse(`(CallExpr (Symbol "fmt.Sprintf") [format arg])`)
func run(pass *analysis.Pass) (any, error) {
for node, m := range code.Matches(pass, checkRedundantSprintfQ) {
format := m.State["format"].(ast.Expr)
arg := m.State["arg"].(ast.Expr)
// TODO(dh): should we really support named constants here?
// shouldn't we only look for string literals? to avoid false
// positives via build tags?
if s, ok := code.ExprToString(pass, format); !ok || s != "%s" {
continue
}
typ := pass.TypesInfo.TypeOf(arg)
if typeparams.IsTypeParam(typ) {
continue
}
irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg
if typeutil.IsTypeWithName(typ, "reflect.Value") {
// printing with %s produces output different from using
// the String method
continue
}
if isFormatter(typ, &irpkg.Prog.MethodSets) {
// the type may choose to handle %s in arbitrary ways
continue
}
if types.Implements(typ, knowledge.Interfaces["fmt.Stringer"]) {
replacement := &ast.CallExpr{
Fun: &ast.SelectorExpr{
X: arg,
Sel: &ast.Ident{Name: "String"},
},
}
report.Report(pass, node, "should use String() instead of fmt.Sprintf",
report.Fixes(edit.Fix("Replace with call to String method", edit.ReplaceWithNode(pass.Fset, node, replacement))))
} else if types.Unalias(typ) == types.Universe.Lookup("string").Type() {
report.Report(pass, node, "the argument is already a string, there's no need to use fmt.Sprintf",
report.FilterGenerated(),
report.Fixes(edit.Fix("Remove unnecessary call to fmt.Sprintf", edit.ReplaceWithNode(pass.Fset, node, arg))))
} else if typ.Underlying() == types.Universe.Lookup("string").Type() {
replacement := &ast.CallExpr{
Fun: &ast.Ident{Name: "string"},
Args: []ast.Expr{arg},
}
report.Report(pass, node, "the argument's underlying type is a string, should use a simple conversion instead of fmt.Sprintf",
report.FilterGenerated(),
report.Fixes(edit.Fix("Replace with conversion to string", edit.ReplaceWithNode(pass.Fset, node, replacement))))
} else if code.IsOfStringConvertibleByteSlice(pass, arg) {
replacement := &ast.CallExpr{
Fun: &ast.Ident{Name: "string"},
Args: []ast.Expr{arg},
}
report.Report(pass, node, "the argument's underlying type is a slice of bytes, should use a simple conversion instead of fmt.Sprintf",
report.FilterGenerated(),
report.Fixes(edit.Fix("Replace with conversion to string", edit.ReplaceWithNode(pass.Fset, node, replacement))))
}
}
return nil, nil
}
func isFormatter(T types.Type, msCache *typeutil.MethodSetCache) bool {
// TODO(dh): this function also exists in staticcheck/lint.go deduplicate.
ms := msCache.MethodSet(T)
sel := ms.Lookup(nil, "Format")
if sel == nil {
return false
}
fn, ok := sel.Obj().(*types.Func)
if !ok {
// should be unreachable
return false
}
sig := fn.Type().(*types.Signature)
if sig.Params().Len() != 2 {
return false
}
// TODO(dh): check the types of the arguments for more
// precision
if sig.Results().Len() != 0 {
return false
}
return true
}

View File

@@ -0,0 +1,45 @@
package s1028
import (
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1028",
Run: run,
Requires: append([]*analysis.Analyzer{generated.Analyzer}, code.RequiredAnalyzers...),
},
Doc: &lint.RawDocumentation{
Title: `Simplify error construction with \'fmt.Errorf\'`,
Before: `errors.New(fmt.Sprintf(...))`,
After: `fmt.Errorf(...)`,
Since: "2017.1",
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var (
checkErrorsNewSprintfQ = pattern.MustParse(`(CallExpr (Symbol "errors.New") [(CallExpr (Symbol "fmt.Sprintf") args)])`)
checkErrorsNewSprintfR = pattern.MustParse(`(CallExpr (SelectorExpr (Ident "fmt") (Ident "Errorf")) args)`)
)
func run(pass *analysis.Pass) (any, error) {
for node, m := range code.Matches(pass, checkErrorsNewSprintfQ) {
edits := code.EditMatch(pass, node, m, checkErrorsNewSprintfR)
// TODO(dh): the suggested fix may leave an unused import behind
report.Report(pass, node, "should use fmt.Errorf(...) instead of errors.New(fmt.Sprintf(...))",
report.FilterGenerated(),
report.Fixes(edit.Fix("Use fmt.Errorf", edits...)))
}
return nil, nil
}

View File

@@ -0,0 +1,31 @@
package s1029
import (
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/internal/passes/buildir"
"honnef.co/go/tools/internal/sharedcheck"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1029",
Run: sharedcheck.CheckRangeStringRunes,
Requires: []*analysis.Analyzer{buildir.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Range over the string directly`,
Text: `Ranging over a string will yield byte offsets and runes. If the offset
isn't used, this is functionally equivalent to converting the string
to a slice of runes and ranging over that. Ranging directly over the
string will be more performant, however, as it avoids allocating a new
slice, the size of which depends on the length of the string.`,
Before: `for _, r := range []rune(s) {}`,
After: `for _, r := range s {}`,
Since: "2017.1",
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer

View File

@@ -0,0 +1,87 @@
package s1030
import (
"fmt"
"go/ast"
"go/types"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1030",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer, generated.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Use \'bytes.Buffer.String\' or \'bytes.Buffer.Bytes\'`,
Text: `\'bytes.Buffer\' has both a \'String\' and a \'Bytes\' method. It is almost never
necessary to use \'string(buf.Bytes())\' or \'[]byte(buf.String())\' simply
use the other method.
The only exception to this are map lookups. Due to a compiler optimization,
\'m[string(buf.Bytes())]\' is more efficient than \'m[buf.String()]\'.
`,
Since: "2017.1",
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var (
checkBytesBufferConversionsQ = pattern.MustParse(`(CallExpr _ [(CallExpr sel@(SelectorExpr recv _) [])])`)
checkBytesBufferConversionsRs = pattern.MustParse(`(CallExpr (SelectorExpr recv (Ident "String")) [])`)
checkBytesBufferConversionsRb = pattern.MustParse(`(CallExpr (SelectorExpr recv (Ident "Bytes")) [])`)
)
func run(pass *analysis.Pass) (any, error) {
if pass.Pkg.Path() == "bytes" || pass.Pkg.Path() == "bytes_test" {
// The bytes package can use itself however it wants
return nil, nil
}
fn := func(c inspector.Cursor) {
node := c.Node()
m, ok := code.Match(pass, checkBytesBufferConversionsQ, node)
if !ok {
return
}
call := node.(*ast.CallExpr)
sel := m.State["sel"].(*ast.SelectorExpr)
typ := pass.TypesInfo.TypeOf(call.Fun)
if types.Unalias(typ) == types.Universe.Lookup("string").Type() && code.IsCallTo(pass, call.Args[0], "(*bytes.Buffer).Bytes") {
if _, ok := c.Parent().Node().(*ast.IndexExpr); ok {
// Don't flag m[string(buf.Bytes())] thanks to a
// compiler optimization, this is actually faster than
// m[buf.String()]
return
}
report.Report(pass, call, fmt.Sprintf("should use %v.String() instead of %v", report.Render(pass, sel.X), report.Render(pass, call)),
report.FilterGenerated(),
report.Fixes(edit.Fix("Simplify conversion", edit.ReplaceWithPattern(pass.Fset, node, checkBytesBufferConversionsRs, m.State))))
} else if typ, ok := types.Unalias(typ).(*types.Slice); ok &&
types.Unalias(typ.Elem()) == types.Universe.Lookup("byte").Type() &&
code.IsCallTo(pass, call.Args[0], "(*bytes.Buffer).String") {
report.Report(pass, call, fmt.Sprintf("should use %v.Bytes() instead of %v", report.Render(pass, sel.X), report.Render(pass, call)),
report.FilterGenerated(),
report.Fixes(edit.Fix("Simplify conversion", edit.ReplaceWithPattern(pass.Fset, node, checkBytesBufferConversionsRb, m.State))))
}
}
for c := range code.Cursor(pass).Preorder((*ast.CallExpr)(nil)) {
fn(c)
}
return nil, nil
}

View File

@@ -0,0 +1,71 @@
package s1031
import (
"go/types"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1031",
Run: run,
Requires: append([]*analysis.Analyzer{generated.Analyzer}, code.RequiredAnalyzers...),
},
Doc: &lint.RawDocumentation{
Title: `Omit redundant nil check around loop`,
Text: `You can use range on nil slices and maps, the loop will simply never
execute. This makes an additional nil check around the loop
unnecessary.`,
Before: `
if s != nil {
for _, x := range s {
...
}
}`,
After: `
for _, x := range s {
...
}`,
Since: "2017.1",
// MergeIfAll because x might be a channel under some build tags.
// you shouldn't write code like that…
MergeIf: lint.MergeIfAll,
},
})
var Analyzer = SCAnalyzer.Analyzer
var checkNilCheckAroundRangeQ = pattern.MustParse(`
(IfStmt
nil
(BinaryExpr x@(Object _) "!=" (Builtin "nil"))
[(RangeStmt _ _ _ x _)]
nil)`)
func run(pass *analysis.Pass) (any, error) {
for node, m := range code.Matches(pass, checkNilCheckAroundRangeQ) {
ok := typeutil.All(m.State["x"].(types.Object).Type(), func(term *types.Term) bool {
switch term.Type().Underlying().(type) {
case *types.Slice, *types.Map:
return true
case *types.TypeParam, *types.Chan, *types.Pointer, *types.Signature:
return false
default:
lint.ExhaustiveTypeSwitch(term.Type().Underlying())
return false
}
})
if ok {
report.Report(pass, node, "unnecessary nil check around range", report.ShortRange(), report.FilterGenerated())
}
}
return nil, nil
}

View File

@@ -0,0 +1,128 @@
package s1032
import (
"go/ast"
"go/token"
"sort"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/knowledge"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1032",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer, generated.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Use \'sort.Ints(x)\', \'sort.Float64s(x)\', and \'sort.Strings(x)\'`,
Text: `The \'sort.Ints\', \'sort.Float64s\' and \'sort.Strings\' functions are easier to
read than \'sort.Sort(sort.IntSlice(x))\', \'sort.Sort(sort.Float64Slice(x))\'
and \'sort.Sort(sort.StringSlice(x))\'.`,
Before: `sort.Sort(sort.StringSlice(x))`,
After: `sort.Strings(x)`,
Since: "2019.1",
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func isPermissibleSort(pass *analysis.Pass, node ast.Node) bool {
call := node.(*ast.CallExpr)
typeconv, ok := call.Args[0].(*ast.CallExpr)
if !ok {
return true
}
sel, ok := typeconv.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
name := code.SelectorName(pass, sel)
switch name {
case "sort.IntSlice", "sort.Float64Slice", "sort.StringSlice":
default:
return true
}
return false
}
func run(pass *analysis.Pass) (any, error) {
type Error struct {
node ast.Node
msg string
}
var allErrors []Error
fn := func(node ast.Node) {
var body *ast.BlockStmt
switch node := node.(type) {
case *ast.FuncLit:
body = node.Body
case *ast.FuncDecl:
body = node.Body
default:
lint.ExhaustiveTypeSwitch(node)
}
if body == nil {
return
}
var errors []Error
permissible := false
fnSorts := func(node ast.Node) bool {
if permissible {
return false
}
if !code.IsCallTo(pass, node, "sort.Sort") {
return true
}
if isPermissibleSort(pass, node) {
permissible = true
return false
}
call := node.(*ast.CallExpr)
// isPermissibleSort guarantees that this type assertion will succeed
typeconv := call.Args[knowledge.Arg("sort.Sort.data")].(*ast.CallExpr)
sel := typeconv.Fun.(*ast.SelectorExpr)
name := code.SelectorName(pass, sel)
switch name {
case "sort.IntSlice":
errors = append(errors, Error{node, "should use sort.Ints(...) instead of sort.Sort(sort.IntSlice(...))"})
case "sort.Float64Slice":
errors = append(errors, Error{node, "should use sort.Float64s(...) instead of sort.Sort(sort.Float64Slice(...))"})
case "sort.StringSlice":
errors = append(errors, Error{node, "should use sort.Strings(...) instead of sort.Sort(sort.StringSlice(...))"})
}
return true
}
ast.Inspect(body, fnSorts)
if permissible {
return
}
allErrors = append(allErrors, errors...)
}
code.Preorder(pass, fn, (*ast.FuncLit)(nil), (*ast.FuncDecl)(nil))
sort.Slice(allErrors, func(i, j int) bool {
return allErrors[i].node.Pos() < allErrors[j].node.Pos()
})
var prev token.Pos
for _, err := range allErrors {
if err.node.Pos() == prev {
continue
}
prev = err.node.Pos()
report.Report(pass, err.node, err.msg, report.FilterGenerated())
}
return nil, nil
}

View File

@@ -0,0 +1,50 @@
package s1033
import (
"go/ast"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1033",
Run: run,
Requires: append([]*analysis.Analyzer{generated.Analyzer}, code.RequiredAnalyzers...),
},
Doc: &lint.RawDocumentation{
Title: `Unnecessary guard around call to \"delete\"`,
Text: `Calling \'delete\' on a nil map is a no-op.`,
Since: "2019.2",
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var checkGuardedDeleteQ = pattern.MustParse(`
(IfStmt
(AssignStmt
[(Ident "_") ok@(Ident _)]
":="
(IndexExpr m key))
ok
[call@(CallExpr (Builtin "delete") [m key])]
nil)`)
func run(pass *analysis.Pass) (any, error) {
for node, m := range code.Matches(pass, checkGuardedDeleteQ) {
report.Report(pass, node, "unnecessary guard around call to delete",
report.ShortRange(),
report.FilterGenerated(),
report.Fixes(edit.Fix("Remove guard", edit.ReplaceWithNode(pass.Fset, node, m.State["call"].(ast.Node)))))
}
return nil, nil
}

View File

@@ -0,0 +1,113 @@
package s1034
import (
"fmt"
"go/ast"
"go/types"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1034",
Run: run,
Requires: append([]*analysis.Analyzer{generated.Analyzer}, code.RequiredAnalyzers...),
},
Doc: &lint.RawDocumentation{
Title: `Use result of type assertion to simplify cases`,
Since: "2019.2",
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var (
checkSimplifyTypeSwitchQ = pattern.MustParse(`
(TypeSwitchStmt
nil
expr@(TypeAssertExpr ident@(Ident _) _)
body)`)
checkSimplifyTypeSwitchR = pattern.MustParse(`(AssignStmt ident ":=" expr)`)
)
func run(pass *analysis.Pass) (any, error) {
for node, m := range code.Matches(pass, checkSimplifyTypeSwitchQ) {
stmt := node.(*ast.TypeSwitchStmt)
expr := m.State["expr"].(ast.Node)
ident := m.State["ident"].(*ast.Ident)
x := pass.TypesInfo.ObjectOf(ident)
var allOffenders []*ast.TypeAssertExpr
canSuggestFix := true
for _, clause := range stmt.Body.List {
clause := clause.(*ast.CaseClause)
if len(clause.List) != 1 {
continue
}
hasUnrelatedAssertion := false
var offenders []*ast.TypeAssertExpr
ast.Inspect(clause, func(node ast.Node) bool {
assert2, ok := node.(*ast.TypeAssertExpr)
if !ok {
return true
}
ident, ok := assert2.X.(*ast.Ident)
if !ok {
hasUnrelatedAssertion = true
return false
}
if pass.TypesInfo.ObjectOf(ident) != x {
hasUnrelatedAssertion = true
return false
}
if !types.Identical(pass.TypesInfo.TypeOf(clause.List[0]), pass.TypesInfo.TypeOf(assert2.Type)) {
hasUnrelatedAssertion = true
return false
}
offenders = append(offenders, assert2)
return true
})
if !hasUnrelatedAssertion {
// don't flag cases that have other type assertions
// unrelated to the one in the case clause. often
// times, this is done for symmetry, when two
// different values have to be asserted to the same
// type.
allOffenders = append(allOffenders, offenders...)
}
canSuggestFix = canSuggestFix && !hasUnrelatedAssertion
}
if len(allOffenders) != 0 {
var opts []report.Option
for _, offender := range allOffenders {
opts = append(opts, report.Related(offender, "could eliminate this type assertion"))
}
opts = append(opts, report.FilterGenerated())
msg := fmt.Sprintf("assigning the result of this type assertion to a variable (switch %s := %s.(type)) could eliminate type assertions in switch cases",
report.Render(pass, ident), report.Render(pass, ident))
if canSuggestFix {
var edits []analysis.TextEdit
edits = append(edits, edit.ReplaceWithPattern(pass.Fset, expr, checkSimplifyTypeSwitchR, m.State))
for _, offender := range allOffenders {
edits = append(edits, edit.ReplaceWithNode(pass.Fset, offender, offender.X))
}
opts = append(opts, report.Fixes(edit.Fix("Simplify type switch", edits...)))
report.Report(pass, expr, msg, opts...)
} else {
report.Report(pass, expr, msg, opts...)
}
}
}
return nil, nil
}

View File

@@ -0,0 +1,54 @@
package s1035
import (
"fmt"
"go/ast"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1035",
Run: run,
Requires: append([]*analysis.Analyzer{generated.Analyzer}, code.RequiredAnalyzers...),
},
Doc: &lint.RawDocumentation{
Title: `Redundant call to \'net/http.CanonicalHeaderKey\' in method call on \'net/http.Header\'`,
Text: `
The methods on \'net/http.Header\', namely \'Add\', \'Del\', \'Get\'
and \'Set\', already canonicalize the given header name.`,
Since: "2020.1",
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var query = pattern.MustParse(`
(CallExpr
(Symbol
callName@(Or
"(net/http.Header).Add"
"(net/http.Header).Del"
"(net/http.Header).Get"
"(net/http.Header).Set"))
arg@(CallExpr (Symbol "net/http.CanonicalHeaderKey") _):_)`)
func run(pass *analysis.Pass) (any, error) {
for _, m := range code.Matches(pass, query) {
arg := m.State["arg"].(*ast.CallExpr)
report.Report(pass, m.State["arg"].(ast.Expr),
fmt.Sprintf("calling net/http.CanonicalHeaderKey on the 'key' argument of %s is redundant", m.State["callName"].(string)),
report.FilterGenerated(),
report.Fixes(edit.Fix("Remove call to CanonicalHeaderKey", edit.ReplaceWithNode(pass.Fset, arg, arg.Args[0]))))
}
return nil, nil
}

View File

@@ -0,0 +1,88 @@
package s1036
import (
"go/ast"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1036",
Run: run,
Requires: code.RequiredAnalyzers,
},
Doc: &lint.RawDocumentation{
Title: `Unnecessary guard around map access`,
Text: `
When accessing a map key that doesn't exist yet, one receives a zero
value. Often, the zero value is a suitable value, for example when
using append or doing integer math.
The following
if _, ok := m["foo"]; ok {
m["foo"] = append(m["foo"], "bar")
} else {
m["foo"] = []string{"bar"}
}
can be simplified to
m["foo"] = append(m["foo"], "bar")
and
if _, ok := m2["k"]; ok {
m2["k"] += 4
} else {
m2["k"] = 4
}
can be simplified to
m["k"] += 4
`,
Since: "2020.1",
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var checkUnnecessaryGuardQ = pattern.MustParse(`
(Or
(IfStmt
(AssignStmt [(Ident "_") ok@(Ident _)] ":=" indexexpr@(IndexExpr _ _))
ok
set@(AssignStmt indexexpr "=" (CallExpr (Builtin "append") indexexpr:values))
(AssignStmt indexexpr "=" (CompositeLit _ values)))
(IfStmt
(AssignStmt [(Ident "_") ok] ":=" indexexpr@(IndexExpr _ _))
ok
set@(AssignStmt indexexpr "+=" value)
(AssignStmt indexexpr "=" value))
(IfStmt
(AssignStmt [(Ident "_") ok] ":=" indexexpr@(IndexExpr _ _))
ok
set@(IncDecStmt indexexpr "++")
(AssignStmt indexexpr "=" (IntegerLiteral "1"))))`)
func run(pass *analysis.Pass) (any, error) {
for node, m := range code.Matches(pass, checkUnnecessaryGuardQ) {
if code.MayHaveSideEffects(pass, m.State["indexexpr"].(ast.Expr), nil) {
continue
}
report.Report(pass, node, "unnecessary guard around map access",
report.ShortRange(),
report.Fixes(edit.Fix("Simplify map access", edit.ReplaceWithNode(pass.Fset, node, m.State["set"].(ast.Node)))))
}
return nil, nil
}

View File

@@ -0,0 +1,55 @@
package s1037
import (
"go/ast"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1037",
Run: run,
Requires: append([]*analysis.Analyzer{generated.Analyzer}, code.RequiredAnalyzers...),
},
Doc: &lint.RawDocumentation{
Title: `Elaborate way of sleeping`,
Text: `Using a select statement with a single case receiving
from the result of \'time.After\' is a very elaborate way of sleeping that
can much simpler be expressed with a simple call to time.Sleep.`,
Since: "2020.1",
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var (
checkElaborateSleepQ = pattern.MustParse(`(SelectStmt (CommClause (UnaryExpr "<-" (CallExpr (Symbol "time.After") [arg])) body))`)
checkElaborateSleepR = pattern.MustParse(`(CallExpr (SelectorExpr (Ident "time") (Ident "Sleep")) [arg])`)
)
func run(pass *analysis.Pass) (any, error) {
for node, m := range code.Matches(pass, checkElaborateSleepQ) {
if body, ok := m.State["body"].([]ast.Stmt); ok && len(body) == 0 {
report.Report(pass, node, "should use time.Sleep instead of elaborate way of sleeping",
report.ShortRange(),
report.FilterGenerated(),
report.Fixes(edit.Fix("Use time.Sleep", edit.ReplaceWithPattern(pass.Fset, node, checkElaborateSleepR, m.State))))
} else {
// TODO(dh): we could make a suggested fix if the body
// doesn't declare or shadow any identifiers
report.Report(pass, node, "should use time.Sleep instead of elaborate way of sleeping",
report.ShortRange(),
report.FilterGenerated())
}
}
return nil, nil
}

View File

@@ -0,0 +1,190 @@
package s1038
import (
"fmt"
"go/ast"
"go/types"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1038",
Run: run,
Requires: append([]*analysis.Analyzer{generated.Analyzer}, code.RequiredAnalyzers...),
},
Doc: &lint.RawDocumentation{
Title: "Unnecessarily complex way of printing formatted string",
Text: `Instead of using \'fmt.Print(fmt.Sprintf(...))\', one can use \'fmt.Printf(...)\'.`,
Since: "2020.1",
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var (
checkPrintSprintQ = pattern.MustParse(`
(Or
(CallExpr
fn@(Or
(Symbol "fmt.Print")
(Symbol "fmt.Sprint")
(Symbol "fmt.Println")
(Symbol "fmt.Sprintln"))
[(CallExpr (Symbol "fmt.Sprintf") f:_)])
(CallExpr
fn@(Or
(Symbol "fmt.Fprint")
(Symbol "fmt.Fprintln"))
[_ (CallExpr (Symbol "fmt.Sprintf") f:_)]))`)
checkTestingErrorSprintfQ = pattern.MustParse(`
(CallExpr
sel@(SelectorExpr
recv
(Ident
name@(Or
"Error"
"Fatal"
"Fatalln"
"Log"
"Panic"
"Panicln"
"Print"
"Println"
"Skip")))
[(CallExpr (Symbol "fmt.Sprintf") args)])`)
checkLogSprintfQ = pattern.MustParse(`
(CallExpr
(Symbol
(Or
"log.Fatal"
"log.Fatalln"
"log.Panic"
"log.Panicln"
"log.Print"
"log.Println"))
[(CallExpr (Symbol "fmt.Sprintf") args)])`)
checkSprintfMapping = map[string]struct {
recv string
alternative string
}{
"(*testing.common).Error": {"(*testing.common)", "Errorf"},
"(testing.TB).Error": {"(testing.TB)", "Errorf"},
"(*testing.common).Fatal": {"(*testing.common)", "Fatalf"},
"(testing.TB).Fatal": {"(testing.TB)", "Fatalf"},
"(*testing.common).Log": {"(*testing.common)", "Logf"},
"(testing.TB).Log": {"(testing.TB)", "Logf"},
"(*testing.common).Skip": {"(*testing.common)", "Skipf"},
"(testing.TB).Skip": {"(testing.TB)", "Skipf"},
"(*log.Logger).Fatal": {"(*log.Logger)", "Fatalf"},
"(*log.Logger).Fatalln": {"(*log.Logger)", "Fatalf"},
"(*log.Logger).Panic": {"(*log.Logger)", "Panicf"},
"(*log.Logger).Panicln": {"(*log.Logger)", "Panicf"},
"(*log.Logger).Print": {"(*log.Logger)", "Printf"},
"(*log.Logger).Println": {"(*log.Logger)", "Printf"},
"log.Fatal": {"", "log.Fatalf"},
"log.Fatalln": {"", "log.Fatalf"},
"log.Panic": {"", "log.Panicf"},
"log.Panicln": {"", "log.Panicf"},
"log.Print": {"", "log.Printf"},
"log.Println": {"", "log.Printf"},
}
)
func run(pass *analysis.Pass) (any, error) {
fmtPrintf := func(node ast.Node) {
m, ok := code.Match(pass, checkPrintSprintQ, node)
if !ok {
return
}
name := m.State["fn"].(*types.Func).Name()
var msg string
switch name {
case "Print", "Fprint", "Sprint":
newname := name + "f"
msg = fmt.Sprintf("should use fmt.%s instead of fmt.%s(fmt.Sprintf(...))", newname, name)
case "Println", "Fprintln", "Sprintln":
if _, ok := m.State["f"].(*ast.BasicLit); !ok {
// This may be an instance of
// fmt.Println(fmt.Sprintf(arg, ...)) where arg is an
// externally provided format string and the caller
// cannot guarantee that the format string ends with a
// newline.
return
}
newname := name[:len(name)-2] + "f"
msg = fmt.Sprintf("should use fmt.%s instead of fmt.%s(fmt.Sprintf(...)) (but don't forget the newline)", newname, name)
}
report.Report(pass, node, msg,
report.FilterGenerated())
}
methSprintf := func(node ast.Node) {
m, ok := code.Match(pass, checkTestingErrorSprintfQ, node)
if !ok {
return
}
mapped, ok := checkSprintfMapping[code.CallName(pass, node.(*ast.CallExpr))]
if !ok {
return
}
// Ensure that Errorf/Fatalf refer to the right method
recvTV, ok := pass.TypesInfo.Types[m.State["recv"].(ast.Expr)]
if !ok {
return
}
obj, _, _ := types.LookupFieldOrMethod(recvTV.Type, recvTV.Addressable(), nil, mapped.alternative)
f, ok := obj.(*types.Func)
if !ok {
return
}
if typeutil.FuncName(f) != mapped.recv+"."+mapped.alternative {
return
}
alt := &ast.SelectorExpr{
X: m.State["recv"].(ast.Expr),
Sel: &ast.Ident{Name: mapped.alternative},
}
report.Report(pass, node, fmt.Sprintf("should use %s(...) instead of %s(fmt.Sprintf(...))", report.Render(pass, alt), report.Render(pass, m.State["sel"].(*ast.SelectorExpr))))
}
pkgSprintf := func(node ast.Node) {
_, ok := code.Match(pass, checkLogSprintfQ, node)
if !ok {
return
}
callName := code.CallName(pass, node.(*ast.CallExpr))
mapped, ok := checkSprintfMapping[callName]
if !ok {
return
}
report.Report(pass, node, fmt.Sprintf("should use %s(...) instead of %s(fmt.Sprintf(...))", mapped.alternative, callName))
}
fn := func(node ast.Node) {
fmtPrintf(node)
// TODO(dh): add suggested fixes
methSprintf(node)
pkgSprintf(node)
}
if !code.CouldMatchAny(pass, checkLogSprintfQ, checkPrintSprintQ, checkTestingErrorSprintfQ) {
return nil, nil
}
code.Preorder(pass, fn, (*ast.CallExpr)(nil))
return nil, nil
}

View File

@@ -0,0 +1,66 @@
package s1039
import (
"fmt"
"go/ast"
"go/types"
"strings"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1039",
Run: run,
Requires: append([]*analysis.Analyzer{generated.Analyzer}, code.RequiredAnalyzers...),
},
Doc: &lint.RawDocumentation{
Title: `Unnecessary use of \'fmt.Sprint\'`,
Text: `
Calling \'fmt.Sprint\' with a single string argument is unnecessary
and identical to using the string directly.`,
Since: "2020.1",
// MergeIfAll because s might not be a string under all build tags.
// you shouldn't write code like that…
MergeIf: lint.MergeIfAll,
},
})
var Analyzer = SCAnalyzer.Analyzer
var checkSprintLiteralQ = pattern.MustParse(`
(CallExpr
fn@(Or
(Symbol "fmt.Sprint")
(Symbol "fmt.Sprintf"))
[lit@(BasicLit "STRING" _)])`)
func run(pass *analysis.Pass) (any, error) {
// We only flag calls with string literals, not expressions of
// type string, because some people use fmt.Sprint(s) as a pattern
// for copying strings, which may be useful when extracting a small
// substring from a large string.
for node, m := range code.Matches(pass, checkSprintLiteralQ) {
callee := m.State["fn"].(*types.Func)
lit := m.State["lit"].(*ast.BasicLit)
if callee.Name() == "Sprintf" {
if strings.ContainsRune(lit.Value, '%') {
// This might be a format string
continue
}
}
report.Report(pass, node, fmt.Sprintf("unnecessary use of fmt.%s", callee.Name()),
report.FilterGenerated(),
report.Fixes(edit.Fix("Replace with string literal", edit.ReplaceWithNode(pass.Fset, node, lit))))
}
return nil, nil
}

View File

@@ -0,0 +1,73 @@
package s1040
import (
"fmt"
"go/ast"
"go/types"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "S1040",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer, generated.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: "Type assertion to current type",
Text: `The type assertion \'x.(SomeInterface)\', when \'x\' already has type
\'SomeInterface\', can only fail if \'x\' is nil. Usually, this is
left-over code from when \'x\' had a different type and you can safely
delete the type assertion. If you want to check that \'x\' is not nil,
consider being explicit and using an actual \'if x == nil\' comparison
instead of relying on the type assertion panicking.`,
Since: "2021.1",
// MergeIfAll because x might have different types under different build tags.
// You shouldn't write code like that…
MergeIf: lint.MergeIfAll,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
fn := func(node ast.Node) {
expr := node.(*ast.TypeAssertExpr)
if expr.Type == nil {
// skip type switches
//
// TODO(dh): we could flag type switches, too, when a case
// statement has the same type as expr.X however,
// depending on the location of that case, it might behave
// identically to a default branch. we need to think
// carefully about the instances we want to flag. We also
// have to take nil interface values into consideration.
//
// It might make more sense to extend SA4020 to handle
// this.
return
}
t1 := pass.TypesInfo.TypeOf(expr.Type)
t2 := pass.TypesInfo.TypeOf(expr.X)
if types.IsInterface(t1) && types.Identical(t1, t2) {
report.Report(pass, expr,
fmt.Sprintf("type assertion to the same type: %s already has type %s", report.Render(pass, expr.X), report.Render(pass, expr.Type)),
report.FilterGenerated())
}
}
// TODO(dh): add suggested fixes. we need different fixes depending on the context:
// - assignment with 1 or 2 lhs
// - assignment to blank identifiers (as the first, second or both lhs)
// - initializers in if statements, with the same variations as above
code.Preorder(pass, fn, (*ast.TypeAssertExpr)(nil))
return nil, nil
}