github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/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  	"fmt"
    19  	"go/ast"
    20  	"go/token"
    21  	"go/types"
    22  	"regexp"
    23  	"strconv"
    24  	"strings"
    25  	"unicode"
    26  
    27  	"golang.org/x/tools/go/analysis"
    28  	"golang.org/x/tools/go/analysis/passes/atomicalign"
    29  	"golang.org/x/tools/go/analysis/passes/copylock"
    30  	"golang.org/x/tools/go/analysis/passes/deepequalerrors"
    31  	"golang.org/x/tools/go/analysis/passes/nilness"
    32  	"golang.org/x/tools/go/analysis/passes/structtag"
    33  )
    34  
    35  func main() {}
    36  
    37  func New(conf any) ([]*analysis.Analyzer, error) {
    38  	return []*analysis.Analyzer{
    39  		SyzAnalyzer,
    40  		// Some standard analyzers that are not enabled in vet.
    41  		atomicalign.Analyzer,
    42  		copylock.Analyzer,
    43  		deepequalerrors.Analyzer,
    44  		nilness.Analyzer,
    45  		structtag.Analyzer,
    46  	}, nil
    47  }
    48  
    49  var SyzAnalyzer = &analysis.Analyzer{
    50  	Name: "lint",
    51  	Doc:  "custom syzkaller project checks",
    52  	Run:  run,
    53  }
    54  
    55  func run(p *analysis.Pass) (interface{}, error) {
    56  	pass := (*Pass)(p)
    57  	for _, file := range pass.Files {
    58  		stmts := make(map[int]bool)
    59  		ast.Inspect(file, func(n ast.Node) bool {
    60  			if n == nil {
    61  				return true
    62  			}
    63  			stmts[pass.Fset.Position(n.Pos()).Line] = true
    64  			switch n := n.(type) {
    65  			case *ast.BinaryExpr:
    66  				pass.checkStringLenCompare(n)
    67  			case *ast.FuncDecl:
    68  				pass.checkFuncArgs(n)
    69  			case *ast.CallExpr:
    70  				pass.checkFlagDefinition(n)
    71  				pass.checkLogErrorFormat(n)
    72  			case *ast.GenDecl:
    73  				pass.checkVarDecl(n)
    74  			}
    75  			return true
    76  		})
    77  		for _, group := range file.Comments {
    78  			for _, comment := range group.List {
    79  				pass.checkComment(comment, stmts, len(group.List) == 1)
    80  			}
    81  		}
    82  	}
    83  	return nil, nil
    84  }
    85  
    86  type Pass analysis.Pass
    87  
    88  func (pass *Pass) report(pos ast.Node, msg string, args ...interface{}) {
    89  	pass.Report(analysis.Diagnostic{
    90  		Pos:     pos.Pos(),
    91  		Message: fmt.Sprintf(msg, args...),
    92  	})
    93  }
    94  
    95  func (pass *Pass) typ(e ast.Expr) string {
    96  	return pass.TypesInfo.Types[e].Type.String()
    97  }
    98  
    99  // checkComment warns about C++-style multiline comments (we don't use them in the codebase)
   100  // and about "//nospace", "// 	tabs and spaces", two spaces after a period, etc.
   101  // See the following sources for some justification:
   102  // https://pep8.org/#comments
   103  // https://nedbatchelder.com/blog/201401/comments_should_be_sentences.html
   104  // https://www.cultofpedagogy.com/two-spaces-after-period
   105  func (pass *Pass) checkComment(n *ast.Comment, stmts map[int]bool, oneline bool) {
   106  	if strings.HasPrefix(n.Text, "/*") {
   107  		pass.report(n, "Use C-style comments // instead of /* */")
   108  		return
   109  	}
   110  	if specialComment.MatchString(n.Text) {
   111  		return
   112  	}
   113  	if !allowedComments.MatchString(n.Text) {
   114  		pass.report(n, "Use either //<one-or-more-spaces>comment or //<one-or-more-tabs>comment format for comments")
   115  		return
   116  	}
   117  	if strings.Contains(n.Text, ".  ") {
   118  		pass.report(n, "Use one space after a period")
   119  		return
   120  	}
   121  	if !oneline || onelineExceptions.MatchString(n.Text) {
   122  		return
   123  	}
   124  	// The following checks are only done for one-line comments,
   125  	// because multi-line comment blocks are harder to understand.
   126  	standalone := !stmts[pass.Fset.Position(n.Pos()).Line]
   127  	if standalone && lowerCaseComment.MatchString(n.Text) {
   128  		pass.report(n, "Standalone comments should be complete sentences"+
   129  			" with first word capitalized and a period at the end")
   130  	}
   131  	if noPeriodComment.MatchString(n.Text) {
   132  		pass.report(n, "Add a period at the end of the comment")
   133  		return
   134  	}
   135  }
   136  
   137  var (
   138  	allowedComments   = regexp.MustCompile(`^//($|	+[^ 	]| +[^ 	])`)
   139  	noPeriodComment   = regexp.MustCompile(`^// [A-Z][a-z].+[a-z]$`)
   140  	lowerCaseComment  = regexp.MustCompile(`^// [a-z]+ `)
   141  	onelineExceptions = regexp.MustCompile(`// want \"|http:|https:`)
   142  	specialComment    = regexp.MustCompile(`//go:generate|//go:build|//go:embed|//go:linkname|// nolint:`)
   143  )
   144  
   145  // checkStringLenCompare checks for string len comparisons with 0.
   146  // E.g.: if len(str) == 0 {} should be if str == "" {}.
   147  func (pass *Pass) checkStringLenCompare(n *ast.BinaryExpr) {
   148  	if n.Op != token.EQL && n.Op != token.NEQ && n.Op != token.LSS &&
   149  		n.Op != token.GTR && n.Op != token.LEQ && n.Op != token.GEQ {
   150  		return
   151  	}
   152  	if pass.isStringLenCall(n.X) && pass.isIntZeroLiteral(n.Y) ||
   153  		pass.isStringLenCall(n.Y) && pass.isIntZeroLiteral(n.X) {
   154  		pass.report(n, "Compare string with \"\", don't compare len with 0")
   155  	}
   156  }
   157  
   158  func (pass *Pass) isStringLenCall(n ast.Expr) bool {
   159  	call, ok := n.(*ast.CallExpr)
   160  	if !ok || len(call.Args) != 1 {
   161  		return false
   162  	}
   163  	fun, ok := call.Fun.(*ast.Ident)
   164  	if !ok || fun.Name != "len" {
   165  		return false
   166  	}
   167  	return pass.typ(call.Args[0]) == "string"
   168  }
   169  
   170  func (pass *Pass) isIntZeroLiteral(n ast.Expr) bool {
   171  	lit, ok := n.(*ast.BasicLit)
   172  	return ok && lit.Kind == token.INT && lit.Value == "0"
   173  }
   174  
   175  // checkFuncArgs checks for "func foo(a int, b int)" -> "func foo(a, b int)".
   176  func (pass *Pass) checkFuncArgs(n *ast.FuncDecl) {
   177  	variadic := pass.TypesInfo.ObjectOf(n.Name).(*types.Func).Type().(*types.Signature).Variadic()
   178  	pass.checkFuncArgList(n.Type.Params.List, variadic)
   179  	if n.Type.Results != nil {
   180  		pass.checkFuncArgList(n.Type.Results.List, false)
   181  	}
   182  }
   183  
   184  func (pass *Pass) checkFuncArgList(fields []*ast.Field, variadic bool) {
   185  	firstBad := -1
   186  	var prev string
   187  	for i, field := range fields {
   188  		if len(field.Names) == 0 {
   189  			pass.reportFuncArgs(fields, firstBad, i)
   190  			firstBad, prev = -1, ""
   191  			continue
   192  		}
   193  		this := pass.typ(field.Type)
   194  		// For variadic functions the actual type of the last argument is a slice,
   195  		// but we don't want to warn on "a []int, b ...int".
   196  		if variadic && i == len(fields)-1 {
   197  			this = "..." + this
   198  		}
   199  		if prev != this {
   200  			pass.reportFuncArgs(fields, firstBad, i)
   201  			firstBad, prev = -1, this
   202  			continue
   203  		}
   204  		if firstBad == -1 {
   205  			firstBad = i - 1
   206  		}
   207  	}
   208  	pass.reportFuncArgs(fields, firstBad, len(fields))
   209  }
   210  
   211  func (pass *Pass) reportFuncArgs(fields []*ast.Field, first, last int) {
   212  	if first == -1 {
   213  		return
   214  	}
   215  	names := ""
   216  	for _, field := range fields[first:last] {
   217  		for _, name := range field.Names {
   218  			names += ", " + name.Name
   219  		}
   220  	}
   221  	pass.report(fields[first], "Use '%v %v'", names[2:], pass.typ(fields[first].Type))
   222  }
   223  
   224  func (pass *Pass) checkFlagDefinition(n *ast.CallExpr) {
   225  	fun, ok := n.Fun.(*ast.SelectorExpr)
   226  	if !ok {
   227  		return
   228  	}
   229  	switch fmt.Sprintf("%v.%v", fun.X, fun.Sel) {
   230  	case "flag.Bool", "flag.Duration", "flag.Float64", "flag.Int", "flag.Int64",
   231  		"flag.String", "flag.Uint", "flag.Uint64":
   232  	default:
   233  		return
   234  	}
   235  	if name, ok := stringLit(n.Args[0]); ok {
   236  		if name != strings.ToLower(name) {
   237  			pass.report(n, "Don't use Capital letters in flag names")
   238  		}
   239  	}
   240  	if desc, ok := stringLit(n.Args[2]); ok {
   241  		if desc == "" {
   242  			pass.report(n, "Provide flag description")
   243  		} else if last := desc[len(desc)-1]; last == '.' || last == '\n' {
   244  			pass.report(n, "Don't use %q at the end of flag description", last)
   245  		}
   246  		if len(desc) >= 2 && unicode.IsUpper(rune(desc[0])) && unicode.IsLower(rune(desc[1])) {
   247  			pass.report(n, "Don't start flag description with a Capital letter")
   248  		}
   249  	}
   250  }
   251  
   252  // checkLogErrorFormat warns about log/error messages starting with capital letter or ending with a period.
   253  func (pass *Pass) checkLogErrorFormat(n *ast.CallExpr) {
   254  	arg, newLine, sure := pass.logFormatArg(n)
   255  	if arg == -1 || len(n.Args) <= arg {
   256  		return
   257  	}
   258  	val, ok := stringLit(n.Args[arg])
   259  	if !ok {
   260  		return
   261  	}
   262  	ln := len(val)
   263  	if ln == 0 {
   264  		pass.report(n, "Don't use empty log/error messages")
   265  		return
   266  	}
   267  	// Some Printf's legitimately don't need \n, so this check is based on a heuristic.
   268  	// Printf's that don't need \n tend to contain % and are short.
   269  	if !sure && ln < 25 && (ln < 10 || strings.Contains(val, "%")) {
   270  		return
   271  	}
   272  	if val[ln-1] == '.' && (ln < 3 || val[ln-2] != '.' || val[ln-3] != '.') {
   273  		pass.report(n, "Don't use period at the end of log/error messages")
   274  	}
   275  	if newLine && val[ln-1] != '\n' {
   276  		pass.report(n, "Add \\n at the end of printed messages")
   277  	}
   278  	if !newLine && val[ln-1] == '\n' {
   279  		pass.report(n, "Don't use \\n at the end of log/error messages")
   280  	}
   281  	if ln >= 2 && unicode.IsUpper(rune(val[0])) && unicode.IsLower(rune(val[1])) &&
   282  		!publicIdentifier.MatchString(val) {
   283  		pass.report(n, "Don't start log/error messages with a Capital letter")
   284  	}
   285  }
   286  
   287  func (pass *Pass) logFormatArg(n *ast.CallExpr) (arg int, newLine, sure bool) {
   288  	fun, ok := n.Fun.(*ast.SelectorExpr)
   289  	if !ok {
   290  		return -1, false, false
   291  	}
   292  	switch fmt.Sprintf("%v.%v", fun.X, fun.Sel) {
   293  	case "log.Print", "log.Printf", "log.Fatal", "log.Fatalf", "fmt.Error", "fmt.Errorf", "jp.Logf":
   294  		return 0, false, true
   295  	case "log.Logf":
   296  		return 1, false, true
   297  	case "fmt.Print", "fmt.Printf":
   298  		return 0, true, false
   299  	case "fmt.Fprint", "fmt.Fprintf":
   300  		if w, ok := n.Args[0].(*ast.SelectorExpr); !ok || fmt.Sprintf("%v.%v", w.X, w.Sel) != "os.Stderr" {
   301  			break
   302  		}
   303  		return 1, true, true
   304  	case "t.Errorf", "t.Fatalf":
   305  		return 0, false, true
   306  	}
   307  	if fun.Sel.String() == "Logf" {
   308  		return 0, false, true
   309  	}
   310  	return -1, false, false
   311  }
   312  
   313  var publicIdentifier = regexp.MustCompile(`^[A-Z][[:alnum:]]+?((\.|[A-Z])[[:alnum:]]+)+ `)
   314  
   315  func stringLit(n ast.Node) (string, bool) {
   316  	lit, ok := n.(*ast.BasicLit)
   317  	if !ok || lit.Kind != token.STRING {
   318  		return "", false
   319  	}
   320  	val, err := strconv.Unquote(lit.Value)
   321  	if err != nil {
   322  		return "", false
   323  	}
   324  	return val, true
   325  }
   326  
   327  // checkVarDecl warns about unnecessary long variable declarations "var x type = foo".
   328  func (pass *Pass) checkVarDecl(n *ast.GenDecl) {
   329  	if n.Tok != token.VAR {
   330  		return
   331  	}
   332  	for _, s := range n.Specs {
   333  		spec, ok := s.(*ast.ValueSpec)
   334  		if !ok || spec.Type == nil || len(spec.Values) == 0 || spec.Names[0].Name == "_" {
   335  			continue
   336  		}
   337  		pass.report(n, "Don't use both var, type and value in variable declarations\n"+
   338  			"Use either \"var x type\" or \"x := val\" or \"x := type(val)\"")
   339  	}
   340  }