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 }