github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/tools/syz-linter/linter.go (about)

     1  // Copyright 2020 syzkaller project authors. All rights reserved.
     2  // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
     3  
     4  // This is our linter with custom checks for the project.
     5  // See the following tutorial on writing Go analyzers:
     6  // https://disaev.me/p/writing-useful-go-analysis-linter/
     7  // See the AST reference see:
     8  // https://pkg.go.dev/go/ast
     9  // https://pkg.go.dev/go/token
    10  // https://pkg.go.dev/go/types
    11  // See the following tutorial on adding custom golangci-lint linters:
    12  // https://golangci-lint.run/contributing/new-linters/
    13  // See comments below and testdata/src/lintertest/lintertest.go for the actual checks we do.
    14  // Note: if you change linter logic, you may need to run "rm -rf ~/.cache/golangci-lint".
    15  package main
    16  
    17  import (
    18  	"bytes"
    19  	"fmt"
    20  	"go/ast"
    21  	"go/printer"
    22  	"go/token"
    23  	"go/types"
    24  	"regexp"
    25  	"strconv"
    26  	"strings"
    27  	"unicode"
    28  
    29  	"golang.org/x/tools/go/analysis"
    30  	"golang.org/x/tools/go/analysis/passes/atomicalign"
    31  	"golang.org/x/tools/go/analysis/passes/copylock"
    32  	"golang.org/x/tools/go/analysis/passes/deepequalerrors"
    33  	"golang.org/x/tools/go/analysis/passes/nilness"
    34  	"golang.org/x/tools/go/analysis/passes/structtag"
    35  )
    36  
    37  func main() {}
    38  
    39  func New(conf any) ([]*analysis.Analyzer, error) {
    40  	return []*analysis.Analyzer{
    41  		SyzAnalyzer,
    42  		// Some standard analyzers that are not enabled in vet.
    43  		atomicalign.Analyzer,
    44  		copylock.Analyzer,
    45  		deepequalerrors.Analyzer,
    46  		nilness.Analyzer,
    47  		structtag.Analyzer,
    48  	}, nil
    49  }
    50  
    51  var SyzAnalyzer = &analysis.Analyzer{
    52  	Name: "lint",
    53  	Doc:  "custom syzkaller project checks",
    54  	Run:  run,
    55  }
    56  
    57  func run(p *analysis.Pass) (interface{}, error) {
    58  	pass := (*Pass)(p)
    59  	for _, file := range pass.Files {
    60  		stmts := make(map[int]bool)
    61  		ast.Inspect(file, func(n ast.Node) bool {
    62  			if n == nil {
    63  				return true
    64  			}
    65  			stmts[pass.Fset.Position(n.Pos()).Line] = true
    66  			switch n := n.(type) {
    67  			case *ast.BinaryExpr:
    68  				pass.checkStringLenCompare(n)
    69  			case *ast.FuncDecl:
    70  				pass.checkFuncArgs(n)
    71  			case *ast.CallExpr:
    72  				pass.checkFlagDefinition(n)
    73  				pass.checkLogErrorFormat(n)
    74  			case *ast.GenDecl:
    75  				pass.checkVarDecl(n)
    76  			case *ast.IfStmt:
    77  				pass.checkIfStmt(n)
    78  			case *ast.AssignStmt:
    79  				pass.checkAssignStmt(n)
    80  			}
    81  			return true
    82  		})
    83  		for _, group := range file.Comments {
    84  			for _, comment := range group.List {
    85  				pass.checkComment(comment, stmts, len(group.List) == 1)
    86  			}
    87  		}
    88  	}
    89  	return nil, nil
    90  }
    91  
    92  type Pass analysis.Pass
    93  
    94  func (pass *Pass) report(pos ast.Node, msg string, args ...interface{}) {
    95  	pass.Report(analysis.Diagnostic{
    96  		Pos:     pos.Pos(),
    97  		Message: fmt.Sprintf(msg, args...),
    98  	})
    99  }
   100  
   101  func (pass *Pass) typ(e ast.Expr) string {
   102  	return pass.TypesInfo.Types[e].Type.String()
   103  }
   104  
   105  // checkComment warns about C++-style multiline comments (we don't use them in the codebase)
   106  // and about "//nospace", "// 	tabs and spaces", two spaces after a period, etc.
   107  // See the following sources for some justification:
   108  // https://pep8.org/#comments
   109  // https://nedbatchelder.com/blog/201401/comments_should_be_sentences.html
   110  // https://www.cultofpedagogy.com/two-spaces-after-period
   111  func (pass *Pass) checkComment(n *ast.Comment, stmts map[int]bool, oneline bool) {
   112  	if strings.HasPrefix(n.Text, "/*") {
   113  		pass.report(n, "Use C-style comments // instead of /* */")
   114  		return
   115  	}
   116  	if specialComment.MatchString(n.Text) {
   117  		return
   118  	}
   119  	if !allowedComments.MatchString(n.Text) {
   120  		pass.report(n, "Use either //<one-or-more-spaces>comment or //<one-or-more-tabs>comment format for comments")
   121  		return
   122  	}
   123  	if strings.Contains(n.Text, ".  ") {
   124  		pass.report(n, "Use one space after a period")
   125  		return
   126  	}
   127  	if !oneline || onelineExceptions.MatchString(n.Text) {
   128  		return
   129  	}
   130  	// The following checks are only done for one-line comments,
   131  	// because multi-line comment blocks are harder to understand.
   132  	standalone := !stmts[pass.Fset.Position(n.Pos()).Line]
   133  	if standalone && lowerCaseComment.MatchString(n.Text) {
   134  		pass.report(n, "Standalone comments should be complete sentences"+
   135  			" with first word capitalized and a period at the end")
   136  	}
   137  	if noPeriodComment.MatchString(n.Text) {
   138  		pass.report(n, "Add a period at the end of the comment")
   139  		return
   140  	}
   141  }
   142  
   143  var (
   144  	allowedComments   = regexp.MustCompile(`^//($|	+[^ 	]| +[^ 	])`)
   145  	noPeriodComment   = regexp.MustCompile(`^// [A-Z][a-z].+[a-z]$`)
   146  	lowerCaseComment  = regexp.MustCompile(`^// [a-z]+ `)
   147  	onelineExceptions = regexp.MustCompile(`// want \"|http:|https:`)
   148  	specialComment    = regexp.MustCompile(`//go:generate|//go:build|//go:embed|//go:linkname|// nolint:`)
   149  )
   150  
   151  // checkStringLenCompare checks for string len comparisons with 0.
   152  // E.g.: if len(str) == 0 {} should be if str == "" {}.
   153  func (pass *Pass) checkStringLenCompare(n *ast.BinaryExpr) {
   154  	if n.Op != token.EQL && n.Op != token.NEQ && n.Op != token.LSS &&
   155  		n.Op != token.GTR && n.Op != token.LEQ && n.Op != token.GEQ {
   156  		return
   157  	}
   158  	if pass.isStringLenCall(n.X) && pass.isIntZeroLiteral(n.Y) ||
   159  		pass.isStringLenCall(n.Y) && pass.isIntZeroLiteral(n.X) {
   160  		pass.report(n, "Compare string with \"\", don't compare len with 0")
   161  	}
   162  }
   163  
   164  func (pass *Pass) isStringLenCall(n ast.Expr) bool {
   165  	call, ok := n.(*ast.CallExpr)
   166  	if !ok || len(call.Args) != 1 {
   167  		return false
   168  	}
   169  	fun, ok := call.Fun.(*ast.Ident)
   170  	if !ok || fun.Name != "len" {
   171  		return false
   172  	}
   173  	return pass.typ(call.Args[0]) == "string"
   174  }
   175  
   176  func (pass *Pass) isIntZeroLiteral(n ast.Expr) bool {
   177  	lit, ok := n.(*ast.BasicLit)
   178  	return ok && lit.Kind == token.INT && lit.Value == "0"
   179  }
   180  
   181  // checkFuncArgs checks for "func foo(a int, b int)" -> "func foo(a, b int)".
   182  func (pass *Pass) checkFuncArgs(n *ast.FuncDecl) {
   183  	variadic := pass.TypesInfo.ObjectOf(n.Name).(*types.Func).Type().(*types.Signature).Variadic()
   184  	pass.checkFuncArgList(n.Type.Params.List, variadic)
   185  	if n.Type.Results != nil {
   186  		pass.checkFuncArgList(n.Type.Results.List, false)
   187  	}
   188  }
   189  
   190  func (pass *Pass) checkFuncArgList(fields []*ast.Field, variadic bool) {
   191  	firstBad := -1
   192  	var prev string
   193  	for i, field := range fields {
   194  		if len(field.Names) == 0 {
   195  			pass.reportFuncArgs(fields, firstBad, i)
   196  			firstBad, prev = -1, ""
   197  			continue
   198  		}
   199  		this := pass.typ(field.Type)
   200  		// For variadic functions the actual type of the last argument is a slice,
   201  		// but we don't want to warn on "a []int, b ...int".
   202  		if variadic && i == len(fields)-1 {
   203  			this = "..." + this
   204  		}
   205  		if prev != this {
   206  			pass.reportFuncArgs(fields, firstBad, i)
   207  			firstBad, prev = -1, this
   208  			continue
   209  		}
   210  		if firstBad == -1 {
   211  			firstBad = i - 1
   212  		}
   213  	}
   214  	pass.reportFuncArgs(fields, firstBad, len(fields))
   215  }
   216  
   217  func (pass *Pass) reportFuncArgs(fields []*ast.Field, first, last int) {
   218  	if first == -1 {
   219  		return
   220  	}
   221  	names := ""
   222  	for _, field := range fields[first:last] {
   223  		for _, name := range field.Names {
   224  			names += ", " + name.Name
   225  		}
   226  	}
   227  	pass.report(fields[first], "Use '%v %v'", names[2:], pass.typ(fields[first].Type))
   228  }
   229  
   230  func (pass *Pass) checkFlagDefinition(n *ast.CallExpr) {
   231  	fun, ok := n.Fun.(*ast.SelectorExpr)
   232  	if !ok {
   233  		return
   234  	}
   235  	switch fmt.Sprintf("%v.%v", fun.X, fun.Sel) {
   236  	case "flag.Bool", "flag.Duration", "flag.Float64", "flag.Int", "flag.Int64",
   237  		"flag.String", "flag.Uint", "flag.Uint64":
   238  	default:
   239  		return
   240  	}
   241  	if name, ok := stringLit(n.Args[0]); ok {
   242  		if name != strings.ToLower(name) {
   243  			pass.report(n, "Don't use Capital letters in flag names")
   244  		}
   245  	}
   246  	if desc, ok := stringLit(n.Args[2]); ok {
   247  		if desc == "" {
   248  			pass.report(n, "Provide flag description")
   249  		} else if last := desc[len(desc)-1]; last == '.' || last == '\n' {
   250  			pass.report(n, "Don't use %q at the end of flag description", last)
   251  		}
   252  		if len(desc) >= 2 && unicode.IsUpper(rune(desc[0])) && unicode.IsLower(rune(desc[1])) {
   253  			pass.report(n, "Don't start flag description with a Capital letter")
   254  		}
   255  	}
   256  }
   257  
   258  // checkLogErrorFormat warns about log/error messages starting with capital letter or ending with a period.
   259  func (pass *Pass) checkLogErrorFormat(n *ast.CallExpr) {
   260  	arg, newLine, sure := pass.logFormatArg(n)
   261  	if arg == -1 || len(n.Args) <= arg {
   262  		return
   263  	}
   264  	val, ok := stringLit(n.Args[arg])
   265  	if !ok {
   266  		return
   267  	}
   268  	ln := len(val)
   269  	if ln == 0 {
   270  		pass.report(n, "Don't use empty log/error messages")
   271  		return
   272  	}
   273  	// Some Printf's legitimately don't need \n, so this check is based on a heuristic.
   274  	// Printf's that don't need \n tend to contain % and are short.
   275  	if !sure && ln < 25 && (ln < 10 || strings.Contains(val, "%")) {
   276  		return
   277  	}
   278  	if val[ln-1] == '.' && (ln < 3 || val[ln-2] != '.' || val[ln-3] != '.') {
   279  		pass.report(n, "Don't use period at the end of log/error messages")
   280  	}
   281  	if newLine && val[ln-1] != '\n' {
   282  		pass.report(n, "Add \\n at the end of printed messages")
   283  	}
   284  	if !newLine && val[ln-1] == '\n' {
   285  		pass.report(n, "Don't use \\n at the end of log/error messages")
   286  	}
   287  	if ln >= 2 && unicode.IsUpper(rune(val[0])) && unicode.IsLower(rune(val[1])) &&
   288  		!publicIdentifier.MatchString(val) {
   289  		pass.report(n, "Don't start log/error messages with a Capital letter")
   290  	}
   291  }
   292  
   293  func (pass *Pass) logFormatArg(n *ast.CallExpr) (arg int, newLine, sure bool) {
   294  	fun, ok := n.Fun.(*ast.SelectorExpr)
   295  	if !ok {
   296  		return -1, false, false
   297  	}
   298  	switch fmt.Sprintf("%v.%v", fun.X, fun.Sel) {
   299  	case "log.Print", "log.Printf", "log.Fatal", "log.Fatalf", "fmt.Error", "fmt.Errorf", "jp.Logf":
   300  		return 0, false, true
   301  	case "log.Logf":
   302  		return 1, false, true
   303  	case "fmt.Print", "fmt.Printf":
   304  		return 0, true, false
   305  	case "fmt.Fprint", "fmt.Fprintf":
   306  		if w, ok := n.Args[0].(*ast.SelectorExpr); !ok || fmt.Sprintf("%v.%v", w.X, w.Sel) != "os.Stderr" {
   307  			break
   308  		}
   309  		return 1, true, true
   310  	case "t.Errorf", "t.Fatalf":
   311  		return 0, false, true
   312  	case "tool.Failf":
   313  		return 0, false, true
   314  	}
   315  	if fun.Sel.String() == "Logf" {
   316  		return 0, false, true
   317  	}
   318  	return -1, false, false
   319  }
   320  
   321  var publicIdentifier = regexp.MustCompile(`^[A-Z][[:alnum:]]+?((\.|[A-Z])[[:alnum:]]+)+ `)
   322  
   323  func stringLit(n ast.Node) (string, bool) {
   324  	lit, ok := n.(*ast.BasicLit)
   325  	if !ok || lit.Kind != token.STRING {
   326  		return "", false
   327  	}
   328  	val, err := strconv.Unquote(lit.Value)
   329  	if err != nil {
   330  		return "", false
   331  	}
   332  	return val, true
   333  }
   334  
   335  // checkVarDecl warns about unnecessary long variable declarations "var x type = foo".
   336  func (pass *Pass) checkVarDecl(n *ast.GenDecl) {
   337  	if n.Tok != token.VAR {
   338  		return
   339  	}
   340  	for _, s := range n.Specs {
   341  		spec, ok := s.(*ast.ValueSpec)
   342  		if !ok || spec.Type == nil || len(spec.Values) == 0 || spec.Names[0].Name == "_" {
   343  			continue
   344  		}
   345  		pass.report(n, "Don't use both var, type and value in variable declarations\n"+
   346  			"Use either \"var x type\" or \"x := val\" or \"x := type(val)\"")
   347  	}
   348  }
   349  
   350  func (pass *Pass) checkIfStmt(n *ast.IfStmt) {
   351  	cond, ok := n.Cond.(*ast.BinaryExpr)
   352  	if !ok || len(n.Body.List) != 1 {
   353  		return
   354  	}
   355  	assign, ok := n.Body.List[0].(*ast.AssignStmt)
   356  	if !ok || assign.Tok != token.ASSIGN || len(assign.Lhs) != 1 {
   357  		return
   358  	}
   359  	isMin := true
   360  	switch cond.Op {
   361  	case token.GTR, token.GEQ:
   362  	case token.LSS, token.LEQ:
   363  		isMin = false
   364  	default:
   365  		return
   366  	}
   367  	x := pass.nodeString(cond.X)
   368  	y := pass.nodeString(cond.Y)
   369  	lhs := pass.nodeString(assign.Lhs[0])
   370  	rhs := pass.nodeString(assign.Rhs[0])
   371  	switch {
   372  	case x == lhs && y == rhs:
   373  	case x == rhs && y == lhs:
   374  		isMin = !isMin
   375  	default:
   376  		return
   377  	}
   378  	fn := map[bool]string{true: "min", false: "max"}[isMin]
   379  	pass.report(n, "Use %v function instead", fn)
   380  }
   381  
   382  func (pass *Pass) nodeString(n ast.Node) string {
   383  	w := new(bytes.Buffer)
   384  	printer.Fprint(w, pass.Fset, n)
   385  	return w.String()
   386  }
   387  
   388  // checkAssignStmt warns about loop variables duplication attempts.
   389  // Before go122 loop variables were per-loop, not per-iter.
   390  func (pass *Pass) checkAssignStmt(n *ast.AssignStmt) {
   391  	if len(n.Lhs) != len(n.Rhs) {
   392  		return
   393  	}
   394  	for i, lhs := range n.Lhs {
   395  		lIdent, ok := lhs.(*ast.Ident)
   396  		if !ok {
   397  			return
   398  		}
   399  		rIdent, ok := n.Rhs[i].(*ast.Ident)
   400  		if !ok {
   401  			return
   402  		}
   403  		if lIdent.Name != rIdent.Name {
   404  			return
   405  		}
   406  	}
   407  	pass.report(n, "Don't duplicate loop variables. They are per-iter (not per-loop) since go122.")
   408  }