1package stylecheck // import "honnef.co/go/tools/stylecheck" 2 3import ( 4 "fmt" 5 "go/ast" 6 "go/constant" 7 "go/token" 8 "go/types" 9 "sort" 10 "strconv" 11 "strings" 12 "unicode" 13 "unicode/utf8" 14 15 "honnef.co/go/tools/code" 16 "honnef.co/go/tools/config" 17 "honnef.co/go/tools/edit" 18 "honnef.co/go/tools/internal/passes/buildir" 19 "honnef.co/go/tools/ir" 20 . "honnef.co/go/tools/lint/lintdsl" 21 "honnef.co/go/tools/pattern" 22 "honnef.co/go/tools/report" 23 24 "golang.org/x/tools/go/analysis" 25 "golang.org/x/tools/go/analysis/passes/inspect" 26 "golang.org/x/tools/go/ast/inspector" 27 "golang.org/x/tools/go/types/typeutil" 28) 29 30func CheckPackageComment(pass *analysis.Pass) (interface{}, error) { 31 // - At least one file in a non-main package should have a package comment 32 // 33 // - The comment should be of the form 34 // "Package x ...". This has a slight potential for false 35 // positives, as multiple files can have package comments, in 36 // which case they get appended. But that doesn't happen a lot in 37 // the real world. 38 39 if pass.Pkg.Name() == "main" { 40 return nil, nil 41 } 42 hasDocs := false 43 for _, f := range pass.Files { 44 if code.IsInTest(pass, f) { 45 continue 46 } 47 if f.Doc != nil && len(f.Doc.List) > 0 { 48 hasDocs = true 49 prefix := "Package " + f.Name.Name + " " 50 if !strings.HasPrefix(strings.TrimSpace(f.Doc.Text()), prefix) { 51 report.Report(pass, f.Doc, fmt.Sprintf(`package comment should be of the form "%s..."`, prefix)) 52 } 53 f.Doc.Text() 54 } 55 } 56 57 if !hasDocs { 58 for _, f := range pass.Files { 59 if code.IsInTest(pass, f) { 60 continue 61 } 62 report.Report(pass, f, "at least one file in a package should have a package comment", report.ShortRange()) 63 } 64 } 65 return nil, nil 66} 67 68func CheckDotImports(pass *analysis.Pass) (interface{}, error) { 69 for _, f := range pass.Files { 70 imports: 71 for _, imp := range f.Imports { 72 path := imp.Path.Value 73 path = path[1 : len(path)-1] 74 for _, w := range config.For(pass).DotImportWhitelist { 75 if w == path { 76 continue imports 77 } 78 } 79 80 if imp.Name != nil && imp.Name.Name == "." && !code.IsInTest(pass, f) { 81 report.Report(pass, imp, "should not use dot imports", report.FilterGenerated()) 82 } 83 } 84 } 85 return nil, nil 86} 87 88func CheckDuplicatedImports(pass *analysis.Pass) (interface{}, error) { 89 for _, f := range pass.Files { 90 // Collect all imports by their import path 91 imports := make(map[string][]*ast.ImportSpec, len(f.Imports)) 92 for _, imp := range f.Imports { 93 imports[imp.Path.Value] = append(imports[imp.Path.Value], imp) 94 } 95 96 for path, value := range imports { 97 if path[1:len(path)-1] == "unsafe" { 98 // Don't flag unsafe. Cgo generated code imports 99 // unsafe using the blank identifier, and most 100 // user-written cgo code also imports unsafe 101 // explicitly. 102 continue 103 } 104 // If there's more than one import per path, we flag that 105 if len(value) > 1 { 106 s := fmt.Sprintf("package %s is being imported more than once", path) 107 opts := []report.Option{report.FilterGenerated()} 108 for _, imp := range value[1:] { 109 opts = append(opts, report.Related(imp, fmt.Sprintf("other import of %s", path))) 110 } 111 report.Report(pass, value[0], s, opts...) 112 } 113 } 114 } 115 return nil, nil 116} 117 118func CheckBlankImports(pass *analysis.Pass) (interface{}, error) { 119 fset := pass.Fset 120 for _, f := range pass.Files { 121 if code.IsMainLike(pass) || code.IsInTest(pass, f) { 122 continue 123 } 124 125 // Collect imports of the form `import _ "foo"`, i.e. with no 126 // parentheses, as their comment will be associated with the 127 // (paren-free) GenDecl, not the import spec itself. 128 // 129 // We don't directly process the GenDecl so that we can 130 // correctly handle the following: 131 // 132 // import _ "foo" 133 // import _ "bar" 134 // 135 // where only the first import should get flagged. 136 skip := map[ast.Spec]bool{} 137 ast.Inspect(f, func(node ast.Node) bool { 138 switch node := node.(type) { 139 case *ast.File: 140 return true 141 case *ast.GenDecl: 142 if node.Tok != token.IMPORT { 143 return false 144 } 145 if node.Lparen == token.NoPos && node.Doc != nil { 146 skip[node.Specs[0]] = true 147 } 148 return false 149 } 150 return false 151 }) 152 for i, imp := range f.Imports { 153 pos := fset.Position(imp.Pos()) 154 155 if !code.IsBlank(imp.Name) { 156 continue 157 } 158 // Only flag the first blank import in a group of imports, 159 // or don't flag any of them, if the first one is 160 // commented 161 if i > 0 { 162 prev := f.Imports[i-1] 163 prevPos := fset.Position(prev.Pos()) 164 if pos.Line-1 == prevPos.Line && code.IsBlank(prev.Name) { 165 continue 166 } 167 } 168 169 if imp.Doc == nil && imp.Comment == nil && !skip[imp] { 170 report.Report(pass, imp, "a blank import should be only in a main or test package, or have a comment justifying it") 171 } 172 } 173 } 174 return nil, nil 175} 176 177func CheckIncDec(pass *analysis.Pass) (interface{}, error) { 178 // TODO(dh): this can be noisy for function bodies that look like this: 179 // x += 3 180 // ... 181 // x += 2 182 // ... 183 // x += 1 184 fn := func(node ast.Node) { 185 assign := node.(*ast.AssignStmt) 186 if assign.Tok != token.ADD_ASSIGN && assign.Tok != token.SUB_ASSIGN { 187 return 188 } 189 if (len(assign.Lhs) != 1 || len(assign.Rhs) != 1) || 190 !code.IsIntLiteral(assign.Rhs[0], "1") { 191 return 192 } 193 194 suffix := "" 195 switch assign.Tok { 196 case token.ADD_ASSIGN: 197 suffix = "++" 198 case token.SUB_ASSIGN: 199 suffix = "--" 200 } 201 202 report.Report(pass, assign, fmt.Sprintf("should replace %s with %s%s", report.Render(pass, assign), report.Render(pass, assign.Lhs[0]), suffix)) 203 } 204 code.Preorder(pass, fn, (*ast.AssignStmt)(nil)) 205 return nil, nil 206} 207 208func CheckErrorReturn(pass *analysis.Pass) (interface{}, error) { 209fnLoop: 210 for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs { 211 sig := fn.Type().(*types.Signature) 212 rets := sig.Results() 213 if rets == nil || rets.Len() < 2 { 214 continue 215 } 216 217 if rets.At(rets.Len()-1).Type() == types.Universe.Lookup("error").Type() { 218 // Last return type is error. If the function also returns 219 // errors in other positions, that's fine. 220 continue 221 } 222 for i := rets.Len() - 2; i >= 0; i-- { 223 if rets.At(i).Type() == types.Universe.Lookup("error").Type() { 224 report.Report(pass, rets.At(i), "error should be returned as the last argument", report.ShortRange()) 225 continue fnLoop 226 } 227 } 228 } 229 return nil, nil 230} 231 232// CheckUnexportedReturn checks that exported functions on exported 233// types do not return unexported types. 234func CheckUnexportedReturn(pass *analysis.Pass) (interface{}, error) { 235 for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs { 236 if fn.Synthetic != "" || fn.Parent() != nil { 237 continue 238 } 239 if !ast.IsExported(fn.Name()) || code.IsMain(pass) || code.IsInTest(pass, fn) { 240 continue 241 } 242 sig := fn.Type().(*types.Signature) 243 if sig.Recv() != nil && !ast.IsExported(code.Dereference(sig.Recv().Type()).(*types.Named).Obj().Name()) { 244 continue 245 } 246 res := sig.Results() 247 for i := 0; i < res.Len(); i++ { 248 if named, ok := code.DereferenceR(res.At(i).Type()).(*types.Named); ok && 249 !ast.IsExported(named.Obj().Name()) && 250 named != types.Universe.Lookup("error").Type() { 251 report.Report(pass, fn, "should not return unexported type") 252 } 253 } 254 } 255 return nil, nil 256} 257 258func CheckReceiverNames(pass *analysis.Pass) (interface{}, error) { 259 irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg 260 for _, m := range irpkg.Members { 261 if T, ok := m.Object().(*types.TypeName); ok && !T.IsAlias() { 262 ms := typeutil.IntuitiveMethodSet(T.Type(), nil) 263 for _, sel := range ms { 264 fn := sel.Obj().(*types.Func) 265 recv := fn.Type().(*types.Signature).Recv() 266 if code.Dereference(recv.Type()) != T.Type() { 267 // skip embedded methods 268 continue 269 } 270 if recv.Name() == "self" || recv.Name() == "this" { 271 report.Report(pass, recv, `receiver name should be a reflection of its identity; don't use generic names such as "this" or "self"`, report.FilterGenerated()) 272 } 273 if recv.Name() == "_" { 274 report.Report(pass, recv, "receiver name should not be an underscore, omit the name if it is unused", report.FilterGenerated()) 275 } 276 } 277 } 278 } 279 return nil, nil 280} 281 282func CheckReceiverNamesIdentical(pass *analysis.Pass) (interface{}, error) { 283 irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg 284 for _, m := range irpkg.Members { 285 names := map[string]int{} 286 287 var firstFn *types.Func 288 if T, ok := m.Object().(*types.TypeName); ok && !T.IsAlias() { 289 ms := typeutil.IntuitiveMethodSet(T.Type(), nil) 290 for _, sel := range ms { 291 fn := sel.Obj().(*types.Func) 292 recv := fn.Type().(*types.Signature).Recv() 293 if code.IsGenerated(pass, recv.Pos()) { 294 // Don't concern ourselves with methods in generated code 295 continue 296 } 297 if code.Dereference(recv.Type()) != T.Type() { 298 // skip embedded methods 299 continue 300 } 301 if firstFn == nil { 302 firstFn = fn 303 } 304 if recv.Name() != "" && recv.Name() != "_" { 305 names[recv.Name()]++ 306 } 307 } 308 } 309 310 if len(names) > 1 { 311 var seen []string 312 for name, count := range names { 313 seen = append(seen, fmt.Sprintf("%dx %q", count, name)) 314 } 315 sort.Strings(seen) 316 317 report.Report(pass, firstFn, fmt.Sprintf("methods on the same type should have the same receiver name (seen %s)", strings.Join(seen, ", "))) 318 } 319 } 320 return nil, nil 321} 322 323func CheckContextFirstArg(pass *analysis.Pass) (interface{}, error) { 324 // TODO(dh): this check doesn't apply to test helpers. Example from the stdlib: 325 // func helperCommandContext(t *testing.T, ctx context.Context, s ...string) (cmd *exec.Cmd) { 326fnLoop: 327 for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs { 328 if fn.Synthetic != "" || fn.Parent() != nil { 329 continue 330 } 331 params := fn.Signature.Params() 332 if params.Len() < 2 { 333 continue 334 } 335 if types.TypeString(params.At(0).Type(), nil) == "context.Context" { 336 continue 337 } 338 for i := 1; i < params.Len(); i++ { 339 param := params.At(i) 340 if types.TypeString(param.Type(), nil) == "context.Context" { 341 report.Report(pass, param, "context.Context should be the first argument of a function", report.ShortRange()) 342 continue fnLoop 343 } 344 } 345 } 346 return nil, nil 347} 348 349func CheckErrorStrings(pass *analysis.Pass) (interface{}, error) { 350 objNames := map[*ir.Package]map[string]bool{} 351 irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg 352 objNames[irpkg] = map[string]bool{} 353 for _, m := range irpkg.Members { 354 if typ, ok := m.(*ir.Type); ok { 355 objNames[irpkg][typ.Name()] = true 356 } 357 } 358 for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs { 359 objNames[fn.Package()][fn.Name()] = true 360 } 361 362 for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs { 363 if code.IsInTest(pass, fn) { 364 // We don't care about malformed error messages in tests; 365 // they're usually for direct human consumption, not part 366 // of an API 367 continue 368 } 369 for _, block := range fn.Blocks { 370 instrLoop: 371 for _, ins := range block.Instrs { 372 call, ok := ins.(*ir.Call) 373 if !ok { 374 continue 375 } 376 if !code.IsCallToAny(call.Common(), "errors.New", "fmt.Errorf") { 377 continue 378 } 379 380 k, ok := call.Common().Args[0].(*ir.Const) 381 if !ok { 382 continue 383 } 384 385 s := constant.StringVal(k.Value) 386 if len(s) == 0 { 387 continue 388 } 389 switch s[len(s)-1] { 390 case '.', ':', '!', '\n': 391 report.Report(pass, call, "error strings should not end with punctuation or a newline") 392 } 393 idx := strings.IndexByte(s, ' ') 394 if idx == -1 { 395 // single word error message, probably not a real 396 // error but something used in tests or during 397 // debugging 398 continue 399 } 400 word := s[:idx] 401 first, n := utf8.DecodeRuneInString(word) 402 if !unicode.IsUpper(first) { 403 continue 404 } 405 for _, c := range word[n:] { 406 if unicode.IsUpper(c) { 407 // Word is probably an initialism or 408 // multi-word function name 409 continue instrLoop 410 } 411 } 412 413 if strings.ContainsRune(word, '(') { 414 // Might be a function call 415 continue instrLoop 416 } 417 word = strings.TrimRightFunc(word, func(r rune) bool { return unicode.IsPunct(r) }) 418 if objNames[fn.Package()][word] { 419 // Word is probably the name of a function or type in this package 420 continue 421 } 422 // First word in error starts with a capital 423 // letter, and the word doesn't contain any other 424 // capitals, making it unlikely to be an 425 // initialism or multi-word function name. 426 // 427 // It could still be a proper noun, though. 428 429 report.Report(pass, call, "error strings should not be capitalized") 430 } 431 } 432 } 433 return nil, nil 434} 435 436func CheckTimeNames(pass *analysis.Pass) (interface{}, error) { 437 suffixes := []string{ 438 "Sec", "Secs", "Seconds", 439 "Msec", "Msecs", 440 "Milli", "Millis", "Milliseconds", 441 "Usec", "Usecs", "Microseconds", 442 "MS", "Ms", 443 } 444 fn := func(names []*ast.Ident) { 445 for _, name := range names { 446 if _, ok := pass.TypesInfo.Defs[name]; !ok { 447 continue 448 } 449 T := pass.TypesInfo.TypeOf(name) 450 if !code.IsType(T, "time.Duration") && !code.IsType(T, "*time.Duration") { 451 continue 452 } 453 for _, suffix := range suffixes { 454 if strings.HasSuffix(name.Name, suffix) { 455 report.Report(pass, name, fmt.Sprintf("var %s is of type %v; don't use unit-specific suffix %q", name.Name, T, suffix)) 456 break 457 } 458 } 459 } 460 } 461 462 fn2 := func(node ast.Node) { 463 switch node := node.(type) { 464 case *ast.ValueSpec: 465 fn(node.Names) 466 case *ast.FieldList: 467 for _, field := range node.List { 468 fn(field.Names) 469 } 470 case *ast.AssignStmt: 471 if node.Tok != token.DEFINE { 472 break 473 } 474 var names []*ast.Ident 475 for _, lhs := range node.Lhs { 476 if lhs, ok := lhs.(*ast.Ident); ok { 477 names = append(names, lhs) 478 } 479 } 480 fn(names) 481 } 482 } 483 484 code.Preorder(pass, fn2, (*ast.ValueSpec)(nil), (*ast.FieldList)(nil), (*ast.AssignStmt)(nil)) 485 return nil, nil 486} 487 488func CheckErrorVarNames(pass *analysis.Pass) (interface{}, error) { 489 for _, f := range pass.Files { 490 for _, decl := range f.Decls { 491 gen, ok := decl.(*ast.GenDecl) 492 if !ok || gen.Tok != token.VAR { 493 continue 494 } 495 for _, spec := range gen.Specs { 496 spec := spec.(*ast.ValueSpec) 497 if len(spec.Names) != len(spec.Values) { 498 continue 499 } 500 501 for i, name := range spec.Names { 502 val := spec.Values[i] 503 if !code.IsCallToAnyAST(pass, val, "errors.New", "fmt.Errorf") { 504 continue 505 } 506 507 if pass.Pkg.Path() == "net/http" && strings.HasPrefix(name.Name, "http2err") { 508 // special case for internal variable names of 509 // bundled HTTP 2 code in net/http 510 continue 511 } 512 prefix := "err" 513 if name.IsExported() { 514 prefix = "Err" 515 } 516 if !strings.HasPrefix(name.Name, prefix) { 517 report.Report(pass, name, fmt.Sprintf("error var %s should have name of the form %sFoo", name.Name, prefix)) 518 } 519 } 520 } 521 } 522 } 523 return nil, nil 524} 525 526var httpStatusCodes = map[int]string{ 527 100: "StatusContinue", 528 101: "StatusSwitchingProtocols", 529 102: "StatusProcessing", 530 200: "StatusOK", 531 201: "StatusCreated", 532 202: "StatusAccepted", 533 203: "StatusNonAuthoritativeInfo", 534 204: "StatusNoContent", 535 205: "StatusResetContent", 536 206: "StatusPartialContent", 537 207: "StatusMultiStatus", 538 208: "StatusAlreadyReported", 539 226: "StatusIMUsed", 540 300: "StatusMultipleChoices", 541 301: "StatusMovedPermanently", 542 302: "StatusFound", 543 303: "StatusSeeOther", 544 304: "StatusNotModified", 545 305: "StatusUseProxy", 546 307: "StatusTemporaryRedirect", 547 308: "StatusPermanentRedirect", 548 400: "StatusBadRequest", 549 401: "StatusUnauthorized", 550 402: "StatusPaymentRequired", 551 403: "StatusForbidden", 552 404: "StatusNotFound", 553 405: "StatusMethodNotAllowed", 554 406: "StatusNotAcceptable", 555 407: "StatusProxyAuthRequired", 556 408: "StatusRequestTimeout", 557 409: "StatusConflict", 558 410: "StatusGone", 559 411: "StatusLengthRequired", 560 412: "StatusPreconditionFailed", 561 413: "StatusRequestEntityTooLarge", 562 414: "StatusRequestURITooLong", 563 415: "StatusUnsupportedMediaType", 564 416: "StatusRequestedRangeNotSatisfiable", 565 417: "StatusExpectationFailed", 566 418: "StatusTeapot", 567 422: "StatusUnprocessableEntity", 568 423: "StatusLocked", 569 424: "StatusFailedDependency", 570 426: "StatusUpgradeRequired", 571 428: "StatusPreconditionRequired", 572 429: "StatusTooManyRequests", 573 431: "StatusRequestHeaderFieldsTooLarge", 574 451: "StatusUnavailableForLegalReasons", 575 500: "StatusInternalServerError", 576 501: "StatusNotImplemented", 577 502: "StatusBadGateway", 578 503: "StatusServiceUnavailable", 579 504: "StatusGatewayTimeout", 580 505: "StatusHTTPVersionNotSupported", 581 506: "StatusVariantAlsoNegotiates", 582 507: "StatusInsufficientStorage", 583 508: "StatusLoopDetected", 584 510: "StatusNotExtended", 585 511: "StatusNetworkAuthenticationRequired", 586} 587 588func CheckHTTPStatusCodes(pass *analysis.Pass) (interface{}, error) { 589 whitelist := map[string]bool{} 590 for _, code := range config.For(pass).HTTPStatusCodeWhitelist { 591 whitelist[code] = true 592 } 593 fn := func(node ast.Node) { 594 call := node.(*ast.CallExpr) 595 596 var arg int 597 switch code.CallNameAST(pass, call) { 598 case "net/http.Error": 599 arg = 2 600 case "net/http.Redirect": 601 arg = 3 602 case "net/http.StatusText": 603 arg = 0 604 case "net/http.RedirectHandler": 605 arg = 1 606 default: 607 return 608 } 609 lit, ok := call.Args[arg].(*ast.BasicLit) 610 if !ok { 611 return 612 } 613 if whitelist[lit.Value] { 614 return 615 } 616 617 n, err := strconv.Atoi(lit.Value) 618 if err != nil { 619 return 620 } 621 s, ok := httpStatusCodes[n] 622 if !ok { 623 return 624 } 625 report.Report(pass, lit, fmt.Sprintf("should use constant http.%s instead of numeric literal %d", s, n), 626 report.FilterGenerated(), 627 report.Fixes(edit.Fix(fmt.Sprintf("use http.%s instead of %d", s, n), edit.ReplaceWithString(pass.Fset, lit, "http."+s)))) 628 } 629 code.Preorder(pass, fn, (*ast.CallExpr)(nil)) 630 return nil, nil 631} 632 633func CheckDefaultCaseOrder(pass *analysis.Pass) (interface{}, error) { 634 fn := func(node ast.Node) { 635 stmt := node.(*ast.SwitchStmt) 636 list := stmt.Body.List 637 for i, c := range list { 638 if c.(*ast.CaseClause).List == nil && i != 0 && i != len(list)-1 { 639 report.Report(pass, c, "default case should be first or last in switch statement", report.FilterGenerated()) 640 break 641 } 642 } 643 } 644 code.Preorder(pass, fn, (*ast.SwitchStmt)(nil)) 645 return nil, nil 646} 647 648var ( 649 checkYodaConditionsQ = pattern.MustParse(`(BinaryExpr left@(BasicLit _ _) tok@(Or "==" "!=") right@(Not (BasicLit _ _)))`) 650 checkYodaConditionsR = pattern.MustParse(`(BinaryExpr right tok left)`) 651) 652 653func CheckYodaConditions(pass *analysis.Pass) (interface{}, error) { 654 fn := func(node ast.Node) { 655 if _, edits, ok := MatchAndEdit(pass, checkYodaConditionsQ, checkYodaConditionsR, node); ok { 656 report.Report(pass, node, "don't use Yoda conditions", 657 report.FilterGenerated(), 658 report.Fixes(edit.Fix("un-Yoda-fy", edits...))) 659 } 660 } 661 code.Preorder(pass, fn, (*ast.BinaryExpr)(nil)) 662 return nil, nil 663} 664 665func CheckInvisibleCharacters(pass *analysis.Pass) (interface{}, error) { 666 fn := func(node ast.Node) { 667 lit := node.(*ast.BasicLit) 668 if lit.Kind != token.STRING { 669 return 670 } 671 672 type invalid struct { 673 r rune 674 off int 675 } 676 var invalids []invalid 677 hasFormat := false 678 hasControl := false 679 for off, r := range lit.Value { 680 if unicode.Is(unicode.Cf, r) { 681 invalids = append(invalids, invalid{r, off}) 682 hasFormat = true 683 } else if unicode.Is(unicode.Cc, r) && r != '\n' && r != '\t' && r != '\r' { 684 invalids = append(invalids, invalid{r, off}) 685 hasControl = true 686 } 687 } 688 689 switch len(invalids) { 690 case 0: 691 return 692 case 1: 693 var kind string 694 if hasFormat { 695 kind = "format" 696 } else if hasControl { 697 kind = "control" 698 } else { 699 panic("unreachable") 700 } 701 702 r := invalids[0] 703 msg := fmt.Sprintf("string literal contains the Unicode %s character %U, consider using the %q escape sequence instead", kind, r.r, r.r) 704 705 replacement := strconv.QuoteRune(r.r) 706 replacement = replacement[1 : len(replacement)-1] 707 edit := analysis.SuggestedFix{ 708 Message: fmt.Sprintf("replace %s character %U with %q", kind, r.r, r.r), 709 TextEdits: []analysis.TextEdit{{ 710 Pos: lit.Pos() + token.Pos(r.off), 711 End: lit.Pos() + token.Pos(r.off) + token.Pos(utf8.RuneLen(r.r)), 712 NewText: []byte(replacement), 713 }}, 714 } 715 delete := analysis.SuggestedFix{ 716 Message: fmt.Sprintf("delete %s character %U", kind, r), 717 TextEdits: []analysis.TextEdit{{ 718 Pos: lit.Pos() + token.Pos(r.off), 719 End: lit.Pos() + token.Pos(r.off) + token.Pos(utf8.RuneLen(r.r)), 720 }}, 721 } 722 report.Report(pass, lit, msg, report.Fixes(edit, delete)) 723 default: 724 var kind string 725 if hasFormat && hasControl { 726 kind = "format and control" 727 } else if hasFormat { 728 kind = "format" 729 } else if hasControl { 730 kind = "control" 731 } else { 732 panic("unreachable") 733 } 734 735 msg := fmt.Sprintf("string literal contains Unicode %s characters, consider using escape sequences instead", kind) 736 var edits []analysis.TextEdit 737 var deletions []analysis.TextEdit 738 for _, r := range invalids { 739 replacement := strconv.QuoteRune(r.r) 740 replacement = replacement[1 : len(replacement)-1] 741 edits = append(edits, analysis.TextEdit{ 742 Pos: lit.Pos() + token.Pos(r.off), 743 End: lit.Pos() + token.Pos(r.off) + token.Pos(utf8.RuneLen(r.r)), 744 NewText: []byte(replacement), 745 }) 746 deletions = append(deletions, analysis.TextEdit{ 747 Pos: lit.Pos() + token.Pos(r.off), 748 End: lit.Pos() + token.Pos(r.off) + token.Pos(utf8.RuneLen(r.r)), 749 }) 750 } 751 edit := analysis.SuggestedFix{ 752 Message: fmt.Sprintf("replace all %s characters with escape sequences", kind), 753 TextEdits: edits, 754 } 755 delete := analysis.SuggestedFix{ 756 Message: fmt.Sprintf("delete all %s characters", kind), 757 TextEdits: deletions, 758 } 759 report.Report(pass, lit, msg, report.Fixes(edit, delete)) 760 } 761 } 762 code.Preorder(pass, fn, (*ast.BasicLit)(nil)) 763 return nil, nil 764} 765 766func CheckExportedFunctionDocs(pass *analysis.Pass) (interface{}, error) { 767 fn := func(node ast.Node) { 768 if code.IsInTest(pass, node) { 769 return 770 } 771 772 decl := node.(*ast.FuncDecl) 773 if decl.Doc == nil { 774 return 775 } 776 if !ast.IsExported(decl.Name.Name) { 777 return 778 } 779 kind := "function" 780 if decl.Recv != nil { 781 kind = "method" 782 switch T := decl.Recv.List[0].Type.(type) { 783 case *ast.StarExpr: 784 if !ast.IsExported(T.X.(*ast.Ident).Name) { 785 return 786 } 787 case *ast.Ident: 788 if !ast.IsExported(T.Name) { 789 return 790 } 791 default: 792 ExhaustiveTypeSwitch(T) 793 } 794 } 795 prefix := decl.Name.Name + " " 796 if !strings.HasPrefix(decl.Doc.Text(), prefix) { 797 report.Report(pass, decl.Doc, fmt.Sprintf(`comment on exported %s %s should be of the form "%s..."`, kind, decl.Name.Name, prefix), report.FilterGenerated()) 798 } 799 } 800 801 code.Preorder(pass, fn, (*ast.FuncDecl)(nil)) 802 return nil, nil 803} 804 805func CheckExportedTypeDocs(pass *analysis.Pass) (interface{}, error) { 806 var genDecl *ast.GenDecl 807 fn := func(node ast.Node, push bool) bool { 808 if !push { 809 genDecl = nil 810 return false 811 } 812 if code.IsInTest(pass, node) { 813 return false 814 } 815 816 switch node := node.(type) { 817 case *ast.GenDecl: 818 if node.Tok == token.IMPORT { 819 return false 820 } 821 genDecl = node 822 return true 823 case *ast.TypeSpec: 824 if !ast.IsExported(node.Name.Name) { 825 return false 826 } 827 828 doc := node.Doc 829 if doc == nil { 830 if len(genDecl.Specs) != 1 { 831 // more than one spec in the GenDecl, don't validate the 832 // docstring 833 return false 834 } 835 if genDecl.Lparen.IsValid() { 836 // 'type ( T )' is weird, don't guess the user's intention 837 return false 838 } 839 doc = genDecl.Doc 840 if doc == nil { 841 return false 842 } 843 } 844 845 s := doc.Text() 846 articles := [...]string{"A", "An", "The"} 847 for _, a := range articles { 848 if strings.HasPrefix(s, a+" ") { 849 s = s[len(a)+1:] 850 break 851 } 852 } 853 if !strings.HasPrefix(s, node.Name.Name+" ") { 854 report.Report(pass, doc, fmt.Sprintf(`comment on exported type %s should be of the form "%s ..." (with optional leading article)`, node.Name.Name, node.Name.Name), report.FilterGenerated()) 855 } 856 return false 857 case *ast.FuncLit, *ast.FuncDecl: 858 return false 859 default: 860 ExhaustiveTypeSwitch(node) 861 return false 862 } 863 } 864 865 pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Nodes([]ast.Node{(*ast.GenDecl)(nil), (*ast.TypeSpec)(nil), (*ast.FuncLit)(nil), (*ast.FuncDecl)(nil)}, fn) 866 return nil, nil 867} 868 869func CheckExportedVarDocs(pass *analysis.Pass) (interface{}, error) { 870 var genDecl *ast.GenDecl 871 fn := func(node ast.Node, push bool) bool { 872 if !push { 873 genDecl = nil 874 return false 875 } 876 if code.IsInTest(pass, node) { 877 return false 878 } 879 880 switch node := node.(type) { 881 case *ast.GenDecl: 882 if node.Tok == token.IMPORT { 883 return false 884 } 885 genDecl = node 886 return true 887 case *ast.ValueSpec: 888 if genDecl.Lparen.IsValid() || len(node.Names) > 1 { 889 // Don't try to guess the user's intention 890 return false 891 } 892 name := node.Names[0].Name 893 if !ast.IsExported(name) { 894 return false 895 } 896 if genDecl.Doc == nil { 897 return false 898 } 899 prefix := name + " " 900 if !strings.HasPrefix(genDecl.Doc.Text(), prefix) { 901 kind := "var" 902 if genDecl.Tok == token.CONST { 903 kind = "const" 904 } 905 report.Report(pass, genDecl.Doc, fmt.Sprintf(`comment on exported %s %s should be of the form "%s..."`, kind, name, prefix), report.FilterGenerated()) 906 } 907 return false 908 case *ast.FuncLit, *ast.FuncDecl: 909 return false 910 default: 911 ExhaustiveTypeSwitch(node) 912 return false 913 } 914 } 915 916 pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Nodes([]ast.Node{(*ast.GenDecl)(nil), (*ast.ValueSpec)(nil), (*ast.FuncLit)(nil), (*ast.FuncDecl)(nil)}, fn) 917 return nil, nil 918} 919