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,34 @@
// Code generated by generate.go. DO NOT EDIT.
package quickfix
import (
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/quickfix/qf1001"
"honnef.co/go/tools/quickfix/qf1002"
"honnef.co/go/tools/quickfix/qf1003"
"honnef.co/go/tools/quickfix/qf1004"
"honnef.co/go/tools/quickfix/qf1005"
"honnef.co/go/tools/quickfix/qf1006"
"honnef.co/go/tools/quickfix/qf1007"
"honnef.co/go/tools/quickfix/qf1008"
"honnef.co/go/tools/quickfix/qf1009"
"honnef.co/go/tools/quickfix/qf1010"
"honnef.co/go/tools/quickfix/qf1011"
"honnef.co/go/tools/quickfix/qf1012"
)
var Analyzers = []*lint.Analyzer{
qf1001.SCAnalyzer,
qf1002.SCAnalyzer,
qf1003.SCAnalyzer,
qf1004.SCAnalyzer,
qf1005.SCAnalyzer,
qf1006.SCAnalyzer,
qf1007.SCAnalyzer,
qf1008.SCAnalyzer,
qf1009.SCAnalyzer,
qf1010.SCAnalyzer,
qf1011.SCAnalyzer,
qf1012.SCAnalyzer,
}

View File

@@ -0,0 +1,9 @@
//go:generate go run ../generate.go
// Package quickfix contains analyzes that implement code refactorings.
// None of these analyzers produce diagnostics that have to be followed.
// Most of the time, they only provide alternative ways of doing things,
// requiring users to make informed decisions.
//
// None of these analyzes should fail a build, and they are likely useless in CI as a whole.
package quickfix

View File

@@ -0,0 +1,123 @@
package qf1001
import (
"go/ast"
"go/types"
"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/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: "QF1001",
Run: CheckDeMorgan,
Requires: []*analysis.Analyzer{inspect.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: "Apply De Morgan's law",
Since: "2021.1",
Severity: lint.SeverityHint,
},
})
var Analyzer = SCAnalyzer.Analyzer
var demorganQ = pattern.MustParse(`(UnaryExpr "!" expr@(BinaryExpr _ _ _))`)
func CheckDeMorgan(pass *analysis.Pass) (any, error) {
// TODO(dh): support going in the other direction, e.g. turning `!a && !b && !c` into `!(a || b || c)`
// hasFloats reports whether any subexpression is of type float.
hasFloats := func(expr ast.Expr) bool {
found := false
ast.Inspect(expr, func(node ast.Node) bool {
if expr, ok := node.(ast.Expr); ok {
if typ := pass.TypesInfo.TypeOf(expr); typ != nil {
if basic, ok := typ.Underlying().(*types.Basic); ok {
if (basic.Info() & types.IsFloat) != 0 {
found = true
return false
}
}
}
}
return true
})
return found
}
for c := range code.Cursor(pass).Preorder((*ast.UnaryExpr)(nil)) {
node := c.Node()
matcher, ok := code.Match(pass, demorganQ, node)
if !ok {
continue
}
expr := matcher.State["expr"].(ast.Expr)
// be extremely conservative when it comes to floats
if hasFloats(expr) {
continue
}
n := astutil.NegateDeMorgan(expr, false)
nr := astutil.NegateDeMorgan(expr, true)
nc, ok := astutil.CopyExpr(n)
if !ok {
continue
}
ns := astutil.SimplifyParentheses(nc)
nrc, ok := astutil.CopyExpr(nr)
if !ok {
continue
}
nrs := astutil.SimplifyParentheses(nrc)
var bn, bnr, bns, bnrs string
switch c.Parent().Node().(type) {
case *ast.BinaryExpr, *ast.IfStmt, *ast.ForStmt, *ast.SwitchStmt:
// Always add parentheses for if, for and switch. If
// they're unnecessary, go/printer will strip them when
// the whole file gets formatted.
bn = report.Render(pass, &ast.ParenExpr{X: n})
bnr = report.Render(pass, &ast.ParenExpr{X: nr})
bns = report.Render(pass, &ast.ParenExpr{X: ns})
bnrs = report.Render(pass, &ast.ParenExpr{X: nrs})
default:
// TODO are there other types where we don't want to strip parentheses?
bn = report.Render(pass, n)
bnr = report.Render(pass, nr)
bns = report.Render(pass, ns)
bnrs = report.Render(pass, nrs)
}
// Note: we cannot compare the ASTs directly, because
// simplifyParentheses might have rebalanced trees without
// affecting the rendered form.
var fixes []analysis.SuggestedFix
fixes = append(fixes, edit.Fix("Apply De Morgan's law", edit.ReplaceWithString(node, bn)))
if bn != bns {
fixes = append(fixes, edit.Fix("Apply De Morgan's law & simplify", edit.ReplaceWithString(node, bns)))
}
if bn != bnr {
fixes = append(fixes, edit.Fix("Apply De Morgan's law recursively", edit.ReplaceWithString(node, bnr)))
if bnr != bnrs {
fixes = append(fixes, edit.Fix("Apply De Morgan's law recursively & simplify", edit.ReplaceWithString(node, bnrs)))
}
}
report.Report(pass, node, "could apply De Morgan's law", report.Fixes(fixes...))
}
return nil, nil
}

View File

@@ -0,0 +1,146 @@
package qf1002
import (
"fmt"
"go/ast"
"go/token"
"strings"
"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/go/ast/astutil"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "QF1002",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: "Convert untagged switch to tagged switch",
Text: `
An untagged switch that compares a single variable against a series of
values can be replaced with a tagged switch.`,
Before: `
switch {
case x == 1 || x == 2, x == 3:
...
case x == 4:
...
default:
...
}`,
After: `
switch x {
case 1, 2, 3:
...
case 4:
...
default:
...
}`,
Since: "2021.1",
Severity: lint.SeverityHint,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
fn := func(node ast.Node) {
swtch := node.(*ast.SwitchStmt)
if swtch.Tag != nil || len(swtch.Body.List) == 0 {
return
}
pairs := make([][]*ast.BinaryExpr, len(swtch.Body.List))
for i, stmt := range swtch.Body.List {
stmt := stmt.(*ast.CaseClause)
for _, cond := range stmt.List {
if !findSwitchPairs(pass, cond, &pairs[i]) {
return
}
}
}
var x ast.Expr
for _, pair := range pairs {
if len(pair) == 0 {
continue
}
if x == nil {
x = pair[0].X
} else {
if !astutil.Equal(x, pair[0].X) {
return
}
}
}
if x == nil {
// the switch only has a default case
if len(pairs) > 1 {
panic("found more than one case clause with no pairs")
}
return
}
edits := make([]analysis.TextEdit, 0, len(swtch.Body.List)+1)
for i, stmt := range swtch.Body.List {
stmt := stmt.(*ast.CaseClause)
if stmt.List == nil {
continue
}
var values []string
for _, binexpr := range pairs[i] {
y := binexpr.Y
if p, ok := y.(*ast.ParenExpr); ok {
y = p.X
}
values = append(values, report.Render(pass, y))
}
edits = append(edits, edit.ReplaceWithString(edit.Range{stmt.List[0].Pos(), stmt.Colon}, strings.Join(values, ", ")))
}
pos := swtch.Body.Lbrace
edits = append(edits, edit.ReplaceWithString(edit.Range{pos, pos}, " "+report.Render(pass, x)))
report.Report(pass, swtch, fmt.Sprintf("could use tagged switch on %s", report.Render(pass, x)),
report.Fixes(edit.Fix("Replace with tagged switch", edits...)),
report.ShortRange())
}
code.Preorder(pass, fn, (*ast.SwitchStmt)(nil))
return nil, nil
}
func findSwitchPairs(pass *analysis.Pass, expr ast.Expr, pairs *[]*ast.BinaryExpr) bool {
binexpr, ok := astutil.Unparen(expr).(*ast.BinaryExpr)
if !ok {
return false
}
switch binexpr.Op {
case token.EQL:
if code.MayHaveSideEffects(pass, binexpr.X, nil) || code.MayHaveSideEffects(pass, binexpr.Y, nil) {
return false
}
// syntactic identity should suffice. we do not allow side
// effects in the case clauses, so there should be no way for
// values to change.
if len(*pairs) > 0 && !astutil.Equal(binexpr.X, (*pairs)[0].X) {
return false
}
*pairs = append(*pairs, binexpr)
return true
case token.LOR:
return findSwitchPairs(pass, binexpr.X, pairs) && findSwitchPairs(pass, binexpr.Y, pairs)
default:
return false
}
}

View File

@@ -0,0 +1,205 @@
package qf1003
import (
"fmt"
"go/ast"
"go/token"
"strings"
"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/go/ast/astutil"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "QF1003",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: "Convert if/else-if chain to tagged switch",
Text: `
A series of if/else-if checks comparing the same variable against
values can be replaced with a tagged switch.`,
Before: `
if x == 1 || x == 2 {
...
} else if x == 3 {
...
} else {
...
}`,
After: `
switch x {
case 1, 2:
...
case 3:
...
default:
...
}`,
Since: "2021.1",
Severity: lint.SeverityInfo,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
nodeLoop:
for c := range code.Cursor(pass).Preorder((*ast.IfStmt)(nil)) {
node := c.Node()
if _, ok := c.Parent().Node().(*ast.IfStmt); ok {
// this if statement is part of an if-else chain
continue
}
ifstmt := node.(*ast.IfStmt)
m := map[ast.Expr][]*ast.BinaryExpr{}
for item := ifstmt; item != nil; {
if item.Init != nil {
continue nodeLoop
}
if item.Body == nil {
continue nodeLoop
}
skip := false
ast.Inspect(item.Body, func(node ast.Node) bool {
if branch, ok := node.(*ast.BranchStmt); ok && branch.Tok != token.GOTO {
skip = true
return false
}
return true
})
if skip {
continue nodeLoop
}
var pairs []*ast.BinaryExpr
if !findSwitchPairs(pass, item.Cond, &pairs) {
continue nodeLoop
}
m[item.Cond] = pairs
switch els := item.Else.(type) {
case *ast.IfStmt:
item = els
case *ast.BlockStmt, nil:
item = nil
default:
panic(fmt.Sprintf("unreachable: %T", els))
}
}
var x ast.Expr
for _, pair := range m {
if len(pair) == 0 {
continue
}
if x == nil {
x = pair[0].X
} else {
if !astutil.Equal(x, pair[0].X) {
continue nodeLoop
}
}
}
if x == nil {
// shouldn't happen
continue nodeLoop
}
// We require at least two 'if' to make this suggestion, to
// avoid clutter in the editor.
if len(m) < 2 {
continue nodeLoop
}
// Note that we insert the switch statement as the first text edit instead of the last one so that gopls has an
// easier time converting it to an LSP-conforming edit.
//
// Specifically:
// > Text edits ranges must never overlap, that means no part of the original
// > document must be manipulated by more than one edit. However, it is
// > possible that multiple edits have the same start position: multiple
// > inserts, or any number of inserts followed by a single remove or replace
// > edit. If multiple inserts have the same position, the order in the array
// > defines the order in which the inserted strings appear in the resulting
// > text.
//
// See https://go.dev/issue/63930
//
// FIXME this edit forces the first case to begin in column 0 because we ignore indentation. try to fix that.
edits := []analysis.TextEdit{edit.ReplaceWithString(edit.Range{ifstmt.If, ifstmt.If}, fmt.Sprintf("switch %s {\n", report.Render(pass, x)))}
for item := ifstmt; item != nil; {
var end token.Pos
if item.Else != nil {
end = item.Else.Pos()
} else {
// delete up to but not including the closing brace.
end = item.Body.Rbrace
}
var conds []string
for _, cond := range m[item.Cond] {
y := cond.Y
if p, ok := y.(*ast.ParenExpr); ok {
y = p.X
}
conds = append(conds, report.Render(pass, y))
}
sconds := strings.Join(conds, ", ")
edits = append(edits,
edit.ReplaceWithString(edit.Range{item.If, item.Body.Lbrace + 1}, "case "+sconds+":"),
edit.Delete(edit.Range{item.Body.Rbrace, end}))
switch els := item.Else.(type) {
case *ast.IfStmt:
item = els
case *ast.BlockStmt:
edits = append(edits, edit.ReplaceWithString(edit.Range{els.Lbrace, els.Lbrace + 1}, "default:"))
item = nil
case nil:
item = nil
default:
panic(fmt.Sprintf("unreachable: %T", els))
}
}
report.Report(pass, ifstmt, fmt.Sprintf("could use tagged switch on %s", report.Render(pass, x)),
report.Fixes(edit.Fix("Replace with tagged switch", edits...)),
report.ShortRange())
}
return nil, nil
}
func findSwitchPairs(pass *analysis.Pass, expr ast.Expr, pairs *[]*ast.BinaryExpr) bool {
binexpr, ok := astutil.Unparen(expr).(*ast.BinaryExpr)
if !ok {
return false
}
switch binexpr.Op {
case token.EQL:
if code.MayHaveSideEffects(pass, binexpr.X, nil) || code.MayHaveSideEffects(pass, binexpr.Y, nil) {
return false
}
// syntactic identity should suffice. we do not allow side
// effects in the case clauses, so there should be no way for
// values to change.
if len(*pairs) > 0 && !astutil.Equal(binexpr.X, (*pairs)[0].X) {
return false
}
*pairs = append(*pairs, binexpr)
return true
case token.LOR:
return findSwitchPairs(pass, binexpr.X, pairs) && findSwitchPairs(pass, binexpr.Y, pairs)
default:
return false
}
}

View File

@@ -0,0 +1,65 @@
package qf1004
import (
"fmt"
"go/ast"
"go/token"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
typeindexanalyzer "honnef.co/go/tools/internal/analysisinternal/typeindex"
"honnef.co/go/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "QF1004",
Run: run,
Requires: []*analysis.Analyzer{typeindexanalyzer.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Use \'strings.ReplaceAll\' instead of \'strings.Replace\' with \'n == -1\'`,
Since: "2021.1",
Severity: lint.SeverityHint,
},
})
var Analyzer = SCAnalyzer.Analyzer
var fns = []struct {
path string
name string
replacement string
}{
{"strings", "Replace", "strings.ReplaceAll"},
{"strings", "SplitN", "strings.Split"},
{"strings", "SplitAfterN", "strings.SplitAfter"},
{"bytes", "Replace", "bytes.ReplaceAll"},
{"bytes", "SplitN", "bytes.Split"},
{"bytes", "SplitAfterN", "bytes.SplitAfter"},
}
func run(pass *analysis.Pass) (any, error) {
// XXX respect minimum Go version
// FIXME(dh): create proper suggested fix for renamed import
index := pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
for _, fn := range fns {
for c := range index.Calls(index.Object(fn.path, fn.name)) {
call := c.Node().(*ast.CallExpr)
if op, ok := call.Args[len(call.Args)-1].(*ast.UnaryExpr); ok && op.Op == token.SUB {
if lit, ok := op.X.(*ast.BasicLit); ok && lit.Value == "1" {
report.Report(pass, call.Fun, fmt.Sprintf("could use %s instead", fn.replacement),
report.Fixes(edit.Fix(fmt.Sprintf("Use %s instead", fn.replacement),
edit.ReplaceWithString(call.Fun, fn.replacement),
edit.Delete(op))))
}
}
}
}
return nil, nil
}

View File

@@ -0,0 +1,111 @@
package qf1005
import (
"go/ast"
"go/constant"
"go/token"
"go/types"
"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/go/ast/astutil"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "QF1005",
Run: run,
Requires: code.RequiredAnalyzers,
},
Doc: &lint.RawDocumentation{
Title: `Expand call to \'math.Pow\'`,
Text: `Some uses of \'math.Pow\' can be simplified to basic multiplication.`,
Before: `math.Pow(x, 2)`,
After: `x * x`,
Since: "2021.1",
Severity: lint.SeverityHint,
},
})
var Analyzer = SCAnalyzer.Analyzer
var mathPowQ = pattern.MustParse(`(CallExpr (Symbol "math.Pow") [x (IntegerLiteral n)])`)
func run(pass *analysis.Pass) (any, error) {
for node, matcher := range code.Matches(pass, mathPowQ) {
x := matcher.State["x"].(ast.Expr)
if code.MayHaveSideEffects(pass, x, nil) {
continue
}
n, ok := constant.Int64Val(constant.ToInt(matcher.State["n"].(types.TypeAndValue).Value))
if !ok {
continue
}
needConversion := false
if T, ok := pass.TypesInfo.Types[x]; ok && T.Value != nil {
info := types.Info{
Types: map[ast.Expr]types.TypeAndValue{},
}
// determine if the constant expression would have type float64 if used on its own
if err := types.CheckExpr(pass.Fset, pass.Pkg, x.Pos(), x, &info); err != nil {
// This should not happen
continue
}
if T, ok := info.Types[x].Type.(*types.Basic); ok {
if T.Kind() != types.UntypedFloat && T.Kind() != types.Float64 {
needConversion = true
}
} else {
needConversion = true
}
}
var replacement ast.Expr
switch n {
case 0:
replacement = &ast.BasicLit{
Kind: token.FLOAT,
Value: "1.0",
}
case 1:
replacement = x
case 2, 3:
r := &ast.BinaryExpr{
X: x,
Op: token.MUL,
Y: x,
}
for i := 3; i <= int(n); i++ {
r = &ast.BinaryExpr{
X: r,
Op: token.MUL,
Y: x,
}
}
rc, ok := astutil.CopyExpr(r)
if !ok {
continue
}
replacement = astutil.SimplifyParentheses(rc)
default:
continue
}
if needConversion && n != 0 {
replacement = &ast.CallExpr{
Fun: &ast.Ident{Name: "float64"},
Args: []ast.Expr{replacement},
}
}
report.Report(pass, node, "could expand call to math.Pow",
report.Fixes(edit.Fix("Expand call to math.Pow", edit.ReplaceWithNode(pass.Fset, node, replacement))))
}
return nil, nil
}

View File

@@ -0,0 +1,61 @@
package qf1006
import (
"go/ast"
"go/token"
"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/go/ast/astutil"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "QF1006",
Run: run,
Requires: code.RequiredAnalyzers,
},
Doc: &lint.RawDocumentation{
Title: `Lift \'if\'+\'break\' into loop condition`,
Before: `
for {
if done {
break
}
...
}`,
After: `
for !done {
...
}`,
Since: "2021.1",
Severity: lint.SeverityHint,
},
})
var Analyzer = SCAnalyzer.Analyzer
var checkForLoopIfBreak = pattern.MustParse(`(ForStmt nil nil nil if@(IfStmt nil cond (BranchStmt "BREAK" nil) nil):_)`)
func run(pass *analysis.Pass) (any, error) {
for node, m := range code.Matches(pass, checkForLoopIfBreak) {
pos := node.Pos() + token.Pos(len("for"))
r := astutil.NegateDeMorgan(m.State["cond"].(ast.Expr), false)
// FIXME(dh): we're leaving behind an empty line when we
// delete the old if statement. However, we can't just delete
// an additional character, in case there closing curly brace
// is followed by a comment, or Windows newlines.
report.Report(pass, m.State["if"].(ast.Node), "could lift into loop condition",
report.Fixes(edit.Fix("Lift into loop condition",
edit.ReplaceWithString(edit.Range{pos, pos}, " "+report.Render(pass, r)),
edit.Delete(m.State["if"].(ast.Node)))))
}
return nil, nil
}

View File

@@ -0,0 +1,92 @@
package qf1007
import (
"go/ast"
"go/token"
"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"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "QF1007",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: "Merge conditional assignment into variable declaration",
Before: `
x := false
if someCondition {
x = true
}`,
After: `x := someCondition`,
Since: "2021.1",
Severity: lint.SeverityHint,
},
})
var Analyzer = SCAnalyzer.Analyzer
var checkConditionalAssignmentQ = pattern.MustParse(`(AssignStmt x@(Object _) ":=" assign@(Builtin b@(Or "true" "false")))`)
var checkConditionalAssignmentIfQ = pattern.MustParse(`(IfStmt nil cond [(AssignStmt x@(Object _) "=" (Builtin b@(Or "true" "false")))] nil)`)
func run(pass *analysis.Pass) (any, error) {
fn := func(node ast.Node) {
var body *ast.BlockStmt
switch node := node.(type) {
case *ast.FuncDecl:
body = node.Body
case *ast.FuncLit:
body = node.Body
default:
panic("unreachable")
}
if body == nil {
return
}
stmts := body.List
if len(stmts) < 2 {
return
}
for i, first := range stmts[:len(stmts)-1] {
second := stmts[i+1]
m1, ok := code.Match(pass, checkConditionalAssignmentQ, first)
if !ok {
continue
}
m2, ok := code.Match(pass, checkConditionalAssignmentIfQ, second)
if !ok {
continue
}
if m1.State["x"] != m2.State["x"] {
continue
}
if m1.State["b"] == m2.State["b"] {
continue
}
v := m2.State["cond"].(ast.Expr)
if m1.State["b"] == "true" {
v = &ast.UnaryExpr{
Op: token.NOT,
X: v,
}
}
report.Report(pass, first, "could merge conditional assignment into variable declaration",
report.Fixes(edit.Fix("Merge conditional assignment into variable declaration",
edit.ReplaceWithNode(pass.Fset, m1.State["assign"].(ast.Node), v),
edit.Delete(second))))
}
}
code.Preorder(pass, fn, (*ast.FuncDecl)(nil), (*ast.FuncLit)(nil))
return nil, nil
}

View File

@@ -0,0 +1,147 @@
package qf1008
import (
"fmt"
"go/ast"
"go/types"
"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/go/ast/astutil"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "QF1008",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: "Omit embedded fields from selector expression",
Since: "2021.1",
Severity: lint.SeverityHint,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
type Selector struct {
Node *ast.SelectorExpr
X ast.Expr
Fields []*ast.Ident
}
// extractSelectors extracts uninterrupted sequences of selector expressions.
// For example, for a.b.c().d.e[0].f.g three sequences will be returned: (X=a, X.b.c), (X=a.b.c(), X.d.e), and (X=a.b.c().d.e[0], X.f.g)
//
// It returns nil if the provided selector expression is not the root of a set of sequences.
// For example, for a.b.c, if node is b.c, no selectors will be returned.
extractSelectors := func(expr *ast.SelectorExpr) []Selector {
path, _ := astutil.PathEnclosingInterval(code.File(pass, expr), expr.Pos(), expr.Pos())
for i := len(path) - 1; i >= 0; i-- {
if el, ok := path[i].(*ast.SelectorExpr); ok {
if el != expr {
// this expression is a subset of the entire chain, don't look at it.
return nil
}
break
}
}
inChain := false
var out []Selector
for _, el := range path {
if expr, ok := el.(*ast.SelectorExpr); ok {
if !inChain {
inChain = true
out = append(out, Selector{X: expr.X})
}
sel := &out[len(out)-1]
sel.Fields = append(sel.Fields, expr.Sel)
sel.Node = expr
} else if inChain {
inChain = false
}
}
return out
}
fn := func(node ast.Node) {
expr := node.(*ast.SelectorExpr)
if _, ok := expr.X.(*ast.SelectorExpr); !ok {
// Avoid the expensive call to PathEnclosingInterval for the common 1-level deep selector, which cannot be shortened.
return
}
sels := extractSelectors(expr)
if len(sels) == 0 {
return
}
for _, sel := range sels {
fieldLoop:
for base, fields := pass.TypesInfo.TypeOf(sel.X), sel.Fields; len(fields) >= 2; base, fields = pass.TypesInfo.ObjectOf(fields[0]).Type(), fields[1:] {
hop1 := fields[0]
hop2 := fields[1]
// the selector expression might be a qualified identifier, which cannot be simplified
if base == types.Typ[types.Invalid] {
continue fieldLoop
}
// Check if we can skip a field in the chain of selectors.
// We can skip a field 'b' if a.b.c and a.c resolve to the same object and take the same path.
//
// We set addressable to true unconditionally because we've already successfully type-checked the program,
// which means either the selector doesn't need addressability, or it is addressable.
leftObj, leftLeg, _ := types.LookupFieldOrMethod(base, true, pass.Pkg, hop1.Name)
// We can't skip fields that aren't embedded
if !leftObj.(*types.Var).Embedded() {
continue fieldLoop
}
directObj, directPath, _ := types.LookupFieldOrMethod(base, true, pass.Pkg, hop2.Name)
// Fail fast if omitting the embedded field leads to a different object
if directObj != pass.TypesInfo.ObjectOf(hop2) {
continue fieldLoop
}
_, rightLeg, _ := types.LookupFieldOrMethod(leftObj.Type(), true, pass.Pkg, hop2.Name)
// Fail fast if the paths are obviously different
if len(directPath) != len(leftLeg)+len(rightLeg) {
continue fieldLoop
}
// Make sure that omitting the embedded field will take the same path to the final object.
// Multiple paths involving different fields may lead to the same type-checker object, causing different runtime behavior.
for i := range directPath {
if i < len(leftLeg) {
if leftLeg[i] != directPath[i] {
continue fieldLoop
}
} else {
if rightLeg[i-len(leftLeg)] != directPath[i] {
continue fieldLoop
}
}
}
e := edit.Delete(edit.Range{hop1.Pos(), hop2.Pos()})
report.Report(pass, hop1, fmt.Sprintf("could remove embedded field %q from selector", hop1.Name),
report.Fixes(edit.Fix(fmt.Sprintf("Remove embedded field %q from selector", hop1.Name), e)))
}
}
}
code.Preorder(pass, fn, (*ast.SelectorExpr)(nil))
return nil, nil
}

View File

@@ -0,0 +1,51 @@
package qf1009
import (
"go/ast"
"go/token"
"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"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "QF1009",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Use \'time.Time.Equal\' instead of \'==\' operator`,
Since: "2021.1",
Severity: lint.SeverityInfo,
},
})
var Analyzer = SCAnalyzer.Analyzer
var timeEqualR = pattern.MustParse(`(CallExpr (SelectorExpr lhs (Ident "Equal")) rhs)`)
func run(pass *analysis.Pass) (any, error) {
// FIXME(dh): create proper suggested fix for renamed import
fn := func(node ast.Node) {
expr := node.(*ast.BinaryExpr)
if expr.Op != token.EQL {
return
}
if !code.IsOfTypeWithName(pass, expr.X, "time.Time") || !code.IsOfTypeWithName(pass, expr.Y, "time.Time") {
return
}
report.Report(pass, node, "probably want to use time.Time.Equal instead",
report.Fixes(edit.Fix("Use time.Time.Equal method",
edit.ReplaceWithPattern(pass.Fset, node, timeEqualR, pattern.State{"lhs": expr.X, "rhs": expr.Y}))))
}
code.Preorder(pass, fn, (*ast.BinaryExpr)(nil))
return nil, nil
}

View File

@@ -0,0 +1,75 @@
package qf1010
import (
"go/ast"
"go/types"
"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/knowledge"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "QF1010",
Run: run,
Requires: code.RequiredAnalyzers,
},
Doc: &lint.RawDocumentation{
Title: "Convert slice of bytes to string when printing it",
Since: "2021.1",
Severity: lint.SeverityHint,
},
})
var Analyzer = SCAnalyzer.Analyzer
var byteSlicePrintingQ = pattern.MustParse(`
(Or
(CallExpr
(Symbol (Or
"fmt.Print"
"fmt.Println"
"fmt.Sprint"
"fmt.Sprintln"
"log.Fatal"
"log.Fatalln"
"log.Panic"
"log.Panicln"
"log.Print"
"log.Println"
"(*log.Logger).Fatal"
"(*log.Logger).Fatalln"
"(*log.Logger).Panic"
"(*log.Logger).Panicln"
"(*log.Logger).Print"
"(*log.Logger).Println")) args)
(CallExpr (Symbol (Or
"fmt.Fprint"
"fmt.Fprintln")) _:args))`)
var byteSlicePrintingR = pattern.MustParse(`(CallExpr (Ident "string") [arg])`)
func run(pass *analysis.Pass) (any, error) {
for _, m := range code.Matches(pass, byteSlicePrintingQ) {
args := m.State["args"].([]ast.Expr)
for _, arg := range args {
if !code.IsOfStringConvertibleByteSlice(pass, arg) {
continue
}
if types.Implements(pass.TypesInfo.TypeOf(arg), knowledge.Interfaces["fmt.Stringer"]) {
continue
}
fix := edit.Fix("Convert argument to string", edit.ReplaceWithPattern(pass.Fset, arg, byteSlicePrintingR, pattern.State{"arg": arg}))
report.Report(pass, arg, "could convert argument to string", report.Fixes(fix))
}
}
return nil, nil
}

View File

@@ -0,0 +1,21 @@
package qf1011
import (
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/internal/sharedcheck"
)
func init() {
SCAnalyzer.Analyzer.Name = "QF1011"
}
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: sharedcheck.RedundantTypeInDeclarationChecker("could", true),
Doc: &lint.RawDocumentation{
Title: "Omit redundant type from variable declaration",
Since: "2021.1",
Severity: lint.SeverityHint,
},
})
var Analyzer = SCAnalyzer.Analyzer

View File

@@ -0,0 +1,128 @@
package qf1012
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/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/knowledge"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "QF1012",
Run: run,
Requires: code.RequiredAnalyzers,
},
Doc: &lint.RawDocumentation{
Title: `Use \'fmt.Fprintf(x, ...)\' instead of \'x.Write(fmt.Sprintf(...))\'`,
Since: "2022.1",
Severity: lint.SeverityHint,
},
})
var Analyzer = SCAnalyzer.Analyzer
var (
checkWriteBytesSprintfQ = pattern.MustParse(`
(CallExpr
(SelectorExpr recv (Ident "Write"))
(CallExpr (ArrayType nil (Ident "byte"))
(CallExpr
fn@(Or
(Symbol "fmt.Sprint")
(Symbol "fmt.Sprintf")
(Symbol "fmt.Sprintln"))
args)
))`)
checkWriteStringSprintfQ = pattern.MustParse(`
(CallExpr
(SelectorExpr recv (Ident "WriteString"))
(CallExpr
fn@(Or
(Symbol "fmt.Sprint")
(Symbol "fmt.Sprintf")
(Symbol "fmt.Sprintln"))
args))`)
)
func run(pass *analysis.Pass) (any, error) {
fn := func(node ast.Node) {
getRecv := func(m *pattern.Matcher) (ast.Expr, types.Type) {
recv := m.State["recv"].(ast.Expr)
recvT := pass.TypesInfo.TypeOf(recv)
// Use *N, not N, for the interface check if N
// is a named non-interface type, since the pointer
// has a larger method set (https://staticcheck.dev/issues/1097).
// We assume the receiver expression is addressable
// since otherwise the code wouldn't compile.
if _, ok := types.Unalias(recvT).(*types.Named); ok && !types.IsInterface(recvT) {
recvT = types.NewPointer(recvT)
recv = &ast.UnaryExpr{Op: token.AND, X: recv}
}
return recv, recvT
}
if m, ok := code.Match(pass, checkWriteBytesSprintfQ, node); ok {
recv, recvT := getRecv(m)
if !types.Implements(recvT, knowledge.Interfaces["io.Writer"]) {
return
}
name := m.State["fn"].(*types.Func).Name()
newName := "F" + strings.TrimPrefix(name, "S")
msg := fmt.Sprintf("Use fmt.%s(...) instead of Write([]byte(fmt.%s(...)))", newName, name)
args := m.State["args"].([]ast.Expr)
fix := edit.Fix(msg, edit.ReplaceWithNode(pass.Fset, node, &ast.CallExpr{
Fun: &ast.SelectorExpr{
X: ast.NewIdent("fmt"),
Sel: ast.NewIdent(newName),
},
Args: append([]ast.Expr{recv}, args...),
}))
report.Report(pass, node, msg, report.Fixes(fix))
} else if m, ok := code.Match(pass, checkWriteStringSprintfQ, node); ok {
recv, recvT := getRecv(m)
if !types.Implements(recvT, knowledge.Interfaces["io.StringWriter"]) {
return
}
// The type needs to implement both StringWriter and Writer.
// If it doesn't implement Writer, then we cannot pass it to fmt.Fprint.
if !types.Implements(recvT, knowledge.Interfaces["io.Writer"]) {
return
}
name := m.State["fn"].(*types.Func).Name()
newName := "F" + strings.TrimPrefix(name, "S")
msg := fmt.Sprintf("Use fmt.%s(...) instead of WriteString(fmt.%s(...))", newName, name)
args := m.State["args"].([]ast.Expr)
fix := edit.Fix(msg, edit.ReplaceWithNode(pass.Fset, node, &ast.CallExpr{
Fun: &ast.SelectorExpr{
X: ast.NewIdent("fmt"),
Sel: ast.NewIdent(newName),
},
Args: append([]ast.Expr{recv}, args...),
}))
report.Report(pass, node, msg, report.Fixes(fix))
}
}
if !code.CouldMatchAny(pass, checkWriteBytesSprintfQ, checkWriteStringSprintfQ) {
return nil, nil
}
code.Preorder(pass, fn, (*ast.CallExpr)(nil))
return nil, nil
}