src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

s1008.go (4129B)


      1 package s1008
      2 
      3 import (
      4 	"fmt"
      5 	"go/ast"
      6 	"go/constant"
      7 	"go/token"
      8 	"strings"
      9 
     10 	"honnef.co/go/tools/analysis/code"
     11 	"honnef.co/go/tools/analysis/facts/generated"
     12 	"honnef.co/go/tools/analysis/lint"
     13 	"honnef.co/go/tools/analysis/report"
     14 	"honnef.co/go/tools/pattern"
     15 
     16 	"golang.org/x/tools/go/analysis"
     17 	"golang.org/x/tools/go/analysis/passes/inspect"
     18 )
     19 
     20 var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
     21 	Analyzer: &analysis.Analyzer{
     22 		Name:     "S1008",
     23 		Run:      run,
     24 		Requires: []*analysis.Analyzer{inspect.Analyzer, generated.Analyzer},
     25 	},
     26 	Doc: &lint.RawDocumentation{
     27 		Title: `Simplify returning boolean expression`,
     28 		Before: `
     29 if <expr> {
     30     return true
     31 }
     32 return false`,
     33 		After:   `return <expr>`,
     34 		Since:   "2017.1",
     35 		MergeIf: lint.MergeIfAny,
     36 	},
     37 })
     38 
     39 var Analyzer = SCAnalyzer.Analyzer
     40 
     41 var (
     42 	checkIfReturnQIf  = pattern.MustParse(`(IfStmt nil cond [(ReturnStmt [ret@(Builtin (Or "true" "false"))])] nil)`)
     43 	checkIfReturnQRet = pattern.MustParse(`(ReturnStmt [ret@(Builtin (Or "true" "false"))])`)
     44 )
     45 
     46 func run(pass *analysis.Pass) (any, error) {
     47 	var cm ast.CommentMap
     48 	fn := func(node ast.Node) {
     49 		if f, ok := node.(*ast.File); ok {
     50 			cm = ast.NewCommentMap(pass.Fset, f, f.Comments)
     51 			return
     52 		}
     53 
     54 		block := node.(*ast.BlockStmt)
     55 		l := len(block.List)
     56 		if l < 2 {
     57 			return
     58 		}
     59 		n1, n2 := block.List[l-2], block.List[l-1]
     60 
     61 		if len(block.List) >= 3 {
     62 			if _, ok := block.List[l-3].(*ast.IfStmt); ok {
     63 				// Do not flag a series of if statements
     64 				return
     65 			}
     66 		}
     67 		m1, ok := code.Match(pass, checkIfReturnQIf, n1)
     68 		if !ok {
     69 			return
     70 		}
     71 		m2, ok := code.Match(pass, checkIfReturnQRet, n2)
     72 		if !ok {
     73 			return
     74 		}
     75 
     76 		if op, ok := m1.State["cond"].(*ast.BinaryExpr); ok {
     77 			switch op.Op {
     78 			case token.EQL, token.LSS, token.GTR, token.NEQ, token.LEQ, token.GEQ:
     79 			default:
     80 				return
     81 			}
     82 		}
     83 
     84 		ret1 := m1.State["ret"].(*ast.Ident)
     85 		ret2 := m2.State["ret"].(*ast.Ident)
     86 		if ret1.Name == ret2.Name {
     87 			// we want the function to return true and false, not the
     88 			// same value both times.
     89 			return
     90 		}
     91 
     92 		hasComments := func(n ast.Node) bool {
     93 			cmf := cm.Filter(n)
     94 			for _, groups := range cmf {
     95 				for _, group := range groups {
     96 					for _, cmt := range group.List {
     97 						if strings.HasPrefix(cmt.Text, "//@ diag") {
     98 							// Staticcheck test cases use comments to mark
     99 							// expected diagnostics. Ignore these comments so we
    100 							// can test this check.
    101 							continue
    102 						}
    103 						return true
    104 					}
    105 				}
    106 			}
    107 			return false
    108 		}
    109 
    110 		// Don't flag if either branch is commented
    111 		if hasComments(n1) || hasComments(n2) {
    112 			return
    113 		}
    114 
    115 		cond := m1.State["cond"].(ast.Expr)
    116 		origCond := cond
    117 		if ret1.Name == "false" {
    118 			cond = negate(pass, cond)
    119 		}
    120 		report.Report(pass, n1,
    121 			fmt.Sprintf("should use 'return %s' instead of 'if %s { return %s }; return %s'",
    122 				report.Render(pass, cond),
    123 				report.Render(pass, origCond), report.Render(pass, ret1), report.Render(pass, ret2)),
    124 			report.FilterGenerated())
    125 	}
    126 	code.Preorder(pass, fn, (*ast.File)(nil), (*ast.BlockStmt)(nil))
    127 	return nil, nil
    128 }
    129 
    130 func negate(pass *analysis.Pass, expr ast.Expr) ast.Expr {
    131 	switch expr := expr.(type) {
    132 	case *ast.BinaryExpr:
    133 		out := *expr
    134 		switch expr.Op {
    135 		case token.EQL:
    136 			out.Op = token.NEQ
    137 		case token.LSS:
    138 			out.Op = token.GEQ
    139 		case token.GTR:
    140 			// Some builtins never return negative ints; "len(x) <= 0" should be "len(x) == 0".
    141 			if call, ok := expr.X.(*ast.CallExpr); ok &&
    142 				code.IsCallToAny(pass, call, "len", "cap", "copy") &&
    143 				code.IsIntegerLiteral(pass, expr.Y, constant.MakeInt64(0)) {
    144 				out.Op = token.EQL
    145 			} else {
    146 				out.Op = token.LEQ
    147 			}
    148 		case token.NEQ:
    149 			out.Op = token.EQL
    150 		case token.LEQ:
    151 			out.Op = token.GTR
    152 		case token.GEQ:
    153 			out.Op = token.LSS
    154 		}
    155 		return &out
    156 	case *ast.Ident, *ast.CallExpr, *ast.IndexExpr, *ast.StarExpr:
    157 		return &ast.UnaryExpr{
    158 			Op: token.NOT,
    159 			X:  expr,
    160 		}
    161 	case *ast.UnaryExpr:
    162 		if expr.Op == token.NOT {
    163 			return expr.X
    164 		}
    165 		return &ast.UnaryExpr{
    166 			Op: token.NOT,
    167 			X:  expr,
    168 		}
    169 	default:
    170 		return &ast.UnaryExpr{
    171 			Op: token.NOT,
    172 			X: &ast.ParenExpr{
    173 				X: expr,
    174 			},
    175 		}
    176 	}
    177 }