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 }