package stylecheck import ( "fmt" "go/ast" "go/constant" "go/token" "go/types" "sort" "strconv" "strings" "unicode" "unicode/utf8" "honnef.co/go/tools/analysis/code" "honnef.co/go/tools/analysis/edit" "honnef.co/go/tools/analysis/lint" "honnef.co/go/tools/analysis/report" "honnef.co/go/tools/config" "honnef.co/go/tools/go/ast/astutil" "honnef.co/go/tools/go/ir" "honnef.co/go/tools/go/ir/irutil" "honnef.co/go/tools/go/types/typeutil" "honnef.co/go/tools/internal/passes/buildir" "honnef.co/go/tools/pattern" "golang.org/x/exp/typeparams" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" ) func docText(doc *ast.CommentGroup) (string, bool) { if doc == nil { return "", false } // We trim spaces primarily because of /**/ style comments, which often have leading space. text := strings.TrimSpace(doc.Text()) return text, text != "" } func CheckPackageComment(pass *analysis.Pass) (interface{}, error) { // - At least one file in a non-main package should have a package comment // // - The comment should be of the form // "Package x ...". This has a slight potential for false // positives, as multiple files can have package comments, in // which case they get appended. But that doesn't happen a lot in // the real world. if pass.Pkg.Name() == "main" { return nil, nil } hasDocs := false for _, f := range pass.Files { if code.IsInTest(pass, f) { continue } text, ok := docText(f.Doc) if ok { hasDocs = true prefix := "Package " + f.Name.Name + " " if !strings.HasPrefix(text, prefix) { report.Report(pass, f.Doc, fmt.Sprintf(`package comment should be of the form "%s..."`, prefix)) } } } if !hasDocs { for _, f := range pass.Files { if code.IsInTest(pass, f) { continue } report.Report(pass, f, "at least one file in a package should have a package comment", report.ShortRange()) } } return nil, nil } func CheckDotImports(pass *analysis.Pass) (interface{}, error) { for _, f := range pass.Files { imports: for _, imp := range f.Imports { path := imp.Path.Value path = path[1 : len(path)-1] for _, w := range config.For(pass).DotImportWhitelist { if w == path { continue imports } } if imp.Name != nil && imp.Name.Name == "." && !code.IsInTest(pass, f) { report.Report(pass, imp, "should not use dot imports", report.FilterGenerated()) } } } return nil, nil } func CheckDuplicatedImports(pass *analysis.Pass) (interface{}, error) { for _, f := range pass.Files { // Collect all imports by their import path imports := make(map[string][]*ast.ImportSpec, len(f.Imports)) for _, imp := range f.Imports { imports[imp.Path.Value] = append(imports[imp.Path.Value], imp) } for path, value := range imports { if path[1:len(path)-1] == "unsafe" { // Don't flag unsafe. Cgo generated code imports // unsafe using the blank identifier, and most // user-written cgo code also imports unsafe // explicitly. continue } // If there's more than one import per path, we flag that if len(value) > 1 { s := fmt.Sprintf("package %s is being imported more than once", path) opts := []report.Option{report.FilterGenerated()} for _, imp := range value[1:] { opts = append(opts, report.Related(imp, fmt.Sprintf("other import of %s", path))) } report.Report(pass, value[0], s, opts...) } } } return nil, nil } func CheckBlankImports(pass *analysis.Pass) (interface{}, error) { fset := pass.Fset for _, f := range pass.Files { if code.IsMainLike(pass) || code.IsInTest(pass, f) { continue } // Collect imports of the form `import _ "foo"`, i.e. with no // parentheses, as their comment will be associated with the // (paren-free) GenDecl, not the import spec itself. // // We don't directly process the GenDecl so that we can // correctly handle the following: // // import _ "foo" // import _ "bar" // // where only the first import should get flagged. skip := map[ast.Spec]bool{} ast.Inspect(f, func(node ast.Node) bool { switch node := node.(type) { case *ast.File: return true case *ast.GenDecl: if node.Tok != token.IMPORT { return false } if node.Lparen == token.NoPos && node.Doc != nil { skip[node.Specs[0]] = true } return false } return false }) for i, imp := range f.Imports { pos := fset.Position(imp.Pos()) if !astutil.IsBlank(imp.Name) { continue } // Only flag the first blank import in a group of imports, // or don't flag any of them, if the first one is // commented if i > 0 { prev := f.Imports[i-1] prevPos := fset.Position(prev.Pos()) if pos.Line-1 == prevPos.Line && astutil.IsBlank(prev.Name) { continue } } if imp.Doc == nil && imp.Comment == nil && !skip[imp] { report.Report(pass, imp, "a blank import should be only in a main or test package, or have a comment justifying it") } } } return nil, nil } var checkIncDecQ = pattern.MustParse(`(AssignStmt x tok@(Or "+=" "-=") (BasicLit "INT" "1"))`) func CheckIncDec(pass *analysis.Pass) (interface{}, error) { // TODO(dh): this can be noisy for function bodies that look like this: // x += 3 // ... // x += 2 // ... // x += 1 fn := func(node ast.Node) { m, ok := code.Match(pass, checkIncDecQ, node) if !ok { return } suffix := "" switch m.State["tok"].(token.Token) { case token.ADD_ASSIGN: suffix = "++" case token.SUB_ASSIGN: suffix = "--" } report.Report(pass, node, fmt.Sprintf("should replace %s with %s%s", report.Render(pass, node), report.Render(pass, m.State["x"].(ast.Node)), suffix)) } code.Preorder(pass, fn, (*ast.AssignStmt)(nil)) return nil, nil } func CheckErrorReturn(pass *analysis.Pass) (interface{}, error) { fnLoop: for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs { sig := fn.Type().(*types.Signature) rets := sig.Results() if rets == nil || rets.Len() < 2 { continue } if rets.At(rets.Len()-1).Type() == types.Universe.Lookup("error").Type() { // Last return type is error. If the function also returns // errors in other positions, that's fine. continue } if rets.Len() >= 2 && rets.At(rets.Len()-1).Type() == types.Universe.Lookup("bool").Type() && rets.At(rets.Len()-2).Type() == types.Universe.Lookup("error").Type() { // Accept (..., error, bool) and assume it's a comma-ok function. It's not clear whether the bool should come last or not for these kinds of functions. continue } for i := rets.Len() - 2; i >= 0; i-- { if rets.At(i).Type() == types.Universe.Lookup("error").Type() { report.Report(pass, rets.At(i), "error should be returned as the last argument", report.ShortRange()) continue fnLoop } } } return nil, nil } // CheckUnexportedReturn checks that exported functions on exported // types do not return unexported types. func CheckUnexportedReturn(pass *analysis.Pass) (interface{}, error) { for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs { if fn.Synthetic != 0 || fn.Parent() != nil { continue } if !ast.IsExported(fn.Name()) || code.IsMain(pass) || code.IsInTest(pass, fn) { continue } sig := fn.Type().(*types.Signature) if sig.Recv() != nil && !ast.IsExported(typeutil.Dereference(sig.Recv().Type()).(*types.Named).Obj().Name()) { continue } res := sig.Results() for i := 0; i < res.Len(); i++ { if named, ok := typeutil.DereferenceR(res.At(i).Type()).(*types.Named); ok && !ast.IsExported(named.Obj().Name()) && named != types.Universe.Lookup("error").Type() { report.Report(pass, fn, "should not return unexported type") } } } return nil, nil } func CheckReceiverNames(pass *analysis.Pass) (interface{}, error) { irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg for _, m := range irpkg.Members { if T, ok := m.Object().(*types.TypeName); ok && !T.IsAlias() { ms := typeutil.IntuitiveMethodSet(T.Type(), nil) for _, sel := range ms { fn := sel.Obj().(*types.Func) recv := fn.Type().(*types.Signature).Recv() if typeutil.Dereference(recv.Type()) != T.Type() { // skip embedded methods continue } if recv.Name() == "self" || recv.Name() == "this" { 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()) } if recv.Name() == "_" { report.Report(pass, recv, "receiver name should not be an underscore, omit the name if it is unused", report.FilterGenerated()) } } } } return nil, nil } func CheckReceiverNamesIdentical(pass *analysis.Pass) (interface{}, error) { irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg for _, m := range irpkg.Members { names := map[string]int{} var firstFn *types.Func if T, ok := m.Object().(*types.TypeName); ok && !T.IsAlias() { ms := typeutil.IntuitiveMethodSet(T.Type(), nil) for _, sel := range ms { fn := sel.Obj().(*types.Func) recv := fn.Type().(*types.Signature).Recv() if code.IsGenerated(pass, recv.Pos()) { // Don't concern ourselves with methods in generated code continue } if typeutil.Dereference(recv.Type()) != T.Type() { // skip embedded methods continue } if firstFn == nil { firstFn = fn } if recv.Name() != "" && recv.Name() != "_" { names[recv.Name()]++ } } } if len(names) > 1 { var seen []string for name, count := range names { seen = append(seen, fmt.Sprintf("%dx %q", count, name)) } sort.Strings(seen) report.Report(pass, firstFn, fmt.Sprintf("methods on the same type should have the same receiver name (seen %s)", strings.Join(seen, ", "))) } } return nil, nil } func CheckContextFirstArg(pass *analysis.Pass) (interface{}, error) { // TODO(dh): this check doesn't apply to test helpers. Example from the stdlib: // func helperCommandContext(t *testing.T, ctx context.Context, s ...string) (cmd *exec.Cmd) { fnLoop: for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs { if fn.Synthetic != 0 || fn.Parent() != nil { continue } params := fn.Signature.Params() if params.Len() < 2 { continue } if types.TypeString(params.At(0).Type(), nil) == "context.Context" { continue } for i := 1; i < params.Len(); i++ { param := params.At(i) if types.TypeString(param.Type(), nil) == "context.Context" { report.Report(pass, param, "context.Context should be the first argument of a function", report.ShortRange()) continue fnLoop } } } return nil, nil } func CheckErrorStrings(pass *analysis.Pass) (interface{}, error) { objNames := map[*ir.Package]map[string]bool{} irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg objNames[irpkg] = map[string]bool{} for _, m := range irpkg.Members { if typ, ok := m.(*ir.Type); ok { objNames[irpkg][typ.Name()] = true } } for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs { objNames[fn.Package()][fn.Name()] = true } for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs { if code.IsInTest(pass, fn) { // We don't care about malformed error messages in tests; // they're usually for direct human consumption, not part // of an API continue } for _, block := range fn.Blocks { instrLoop: for _, ins := range block.Instrs { call, ok := ins.(*ir.Call) if !ok { continue } if !irutil.IsCallToAny(call.Common(), "errors.New", "fmt.Errorf") { continue } k, ok := call.Common().Args[0].(*ir.Const) if !ok { continue } s := constant.StringVal(k.Value) if len(s) == 0 { continue } switch s[len(s)-1] { case '.', ':', '!', '\n': report.Report(pass, call, "error strings should not end with punctuation or newlines") } idx := strings.IndexByte(s, ' ') if idx == -1 { // single word error message, probably not a real // error but something used in tests or during // debugging continue } word := s[:idx] first, n := utf8.DecodeRuneInString(word) if !unicode.IsUpper(first) { continue } for _, c := range word[n:] { if unicode.IsUpper(c) { // Word is probably an initialism or // multi-word function name continue instrLoop } } if strings.ContainsRune(word, '(') { // Might be a function call continue instrLoop } word = strings.TrimRightFunc(word, func(r rune) bool { return unicode.IsPunct(r) }) if objNames[fn.Package()][word] { // Word is probably the name of a function or type in this package continue } // First word in error starts with a capital // letter, and the word doesn't contain any other // capitals, making it unlikely to be an // initialism or multi-word function name. // // It could still be a proper noun, though. report.Report(pass, call, "error strings should not be capitalized") } } } return nil, nil } func CheckTimeNames(pass *analysis.Pass) (interface{}, error) { suffixes := []string{ "Sec", "Secs", "Seconds", "Msec", "Msecs", "Milli", "Millis", "Milliseconds", "Usec", "Usecs", "Microseconds", "MS", "Ms", } fn := func(names []*ast.Ident) { for _, name := range names { if _, ok := pass.TypesInfo.Defs[name]; !ok { continue } T := pass.TypesInfo.TypeOf(name) if !typeutil.IsType(T, "time.Duration") && !typeutil.IsType(T, "*time.Duration") { continue } for _, suffix := range suffixes { if strings.HasSuffix(name.Name, suffix) { report.Report(pass, name, fmt.Sprintf("var %s is of type %v; don't use unit-specific suffix %q", name.Name, T, suffix)) break } } } } fn2 := func(node ast.Node) { switch node := node.(type) { case *ast.ValueSpec: fn(node.Names) case *ast.FieldList: for _, field := range node.List { fn(field.Names) } case *ast.AssignStmt: if node.Tok != token.DEFINE { break } var names []*ast.Ident for _, lhs := range node.Lhs { if lhs, ok := lhs.(*ast.Ident); ok { names = append(names, lhs) } } fn(names) } } code.Preorder(pass, fn2, (*ast.ValueSpec)(nil), (*ast.FieldList)(nil), (*ast.AssignStmt)(nil)) return nil, nil } func CheckErrorVarNames(pass *analysis.Pass) (interface{}, error) { for _, f := range pass.Files { for _, decl := range f.Decls { gen, ok := decl.(*ast.GenDecl) if !ok || gen.Tok != token.VAR { continue } for _, spec := range gen.Specs { spec := spec.(*ast.ValueSpec) if len(spec.Names) != len(spec.Values) { continue } for i, name := range spec.Names { val := spec.Values[i] if !code.IsCallToAny(pass, val, "errors.New", "fmt.Errorf") { continue } if pass.Pkg.Path() == "net/http" && strings.HasPrefix(name.Name, "http2err") { // special case for internal variable names of // bundled HTTP 2 code in net/http continue } prefix := "err" if name.IsExported() { prefix = "Err" } if !strings.HasPrefix(name.Name, prefix) { report.Report(pass, name, fmt.Sprintf("error var %s should have name of the form %sFoo", name.Name, prefix)) } } } } } return nil, nil } var httpStatusCodes = map[int64]string{ 100: "StatusContinue", 101: "StatusSwitchingProtocols", 102: "StatusProcessing", 200: "StatusOK", 201: "StatusCreated", 202: "StatusAccepted", 203: "StatusNonAuthoritativeInfo", 204: "StatusNoContent", 205: "StatusResetContent", 206: "StatusPartialContent", 207: "StatusMultiStatus", 208: "StatusAlreadyReported", 226: "StatusIMUsed", 300: "StatusMultipleChoices", 301: "StatusMovedPermanently", 302: "StatusFound", 303: "StatusSeeOther", 304: "StatusNotModified", 305: "StatusUseProxy", 307: "StatusTemporaryRedirect", 308: "StatusPermanentRedirect", 400: "StatusBadRequest", 401: "StatusUnauthorized", 402: "StatusPaymentRequired", 403: "StatusForbidden", 404: "StatusNotFound", 405: "StatusMethodNotAllowed", 406: "StatusNotAcceptable", 407: "StatusProxyAuthRequired", 408: "StatusRequestTimeout", 409: "StatusConflict", 410: "StatusGone", 411: "StatusLengthRequired", 412: "StatusPreconditionFailed", 413: "StatusRequestEntityTooLarge", 414: "StatusRequestURITooLong", 415: "StatusUnsupportedMediaType", 416: "StatusRequestedRangeNotSatisfiable", 417: "StatusExpectationFailed", 418: "StatusTeapot", 422: "StatusUnprocessableEntity", 423: "StatusLocked", 424: "StatusFailedDependency", 426: "StatusUpgradeRequired", 428: "StatusPreconditionRequired", 429: "StatusTooManyRequests", 431: "StatusRequestHeaderFieldsTooLarge", 451: "StatusUnavailableForLegalReasons", 500: "StatusInternalServerError", 501: "StatusNotImplemented", 502: "StatusBadGateway", 503: "StatusServiceUnavailable", 504: "StatusGatewayTimeout", 505: "StatusHTTPVersionNotSupported", 506: "StatusVariantAlsoNegotiates", 507: "StatusInsufficientStorage", 508: "StatusLoopDetected", 510: "StatusNotExtended", 511: "StatusNetworkAuthenticationRequired", } func CheckHTTPStatusCodes(pass *analysis.Pass) (interface{}, error) { whitelist := map[string]bool{} for _, code := range config.For(pass).HTTPStatusCodeWhitelist { whitelist[code] = true } fn := func(node ast.Node) { call := node.(*ast.CallExpr) var arg int switch code.CallName(pass, call) { case "net/http.Error": arg = 2 case "net/http.Redirect": arg = 3 case "net/http.StatusText": arg = 0 case "net/http.RedirectHandler": arg = 1 default: return } if arg >= len(call.Args) { return } tv, ok := code.IntegerLiteral(pass, call.Args[arg]) if !ok { return } n, ok := constant.Int64Val(tv.Value) if !ok { return } if whitelist[strconv.FormatInt(n, 10)] { return } s, ok := httpStatusCodes[n] if !ok { return } lit := call.Args[arg] report.Report(pass, lit, fmt.Sprintf("should use constant http.%s instead of numeric literal %d", s, n), report.FilterGenerated(), report.Fixes(edit.Fix(fmt.Sprintf("use http.%s instead of %d", s, n), edit.ReplaceWithString(lit, "http."+s)))) } code.Preorder(pass, fn, (*ast.CallExpr)(nil)) return nil, nil } func CheckDefaultCaseOrder(pass *analysis.Pass) (interface{}, error) { fn := func(node ast.Node) { stmt := node.(*ast.SwitchStmt) list := stmt.Body.List for i, c := range list { if c.(*ast.CaseClause).List == nil && i != 0 && i != len(list)-1 { report.Report(pass, c, "default case should be first or last in switch statement", report.FilterGenerated()) break } } } code.Preorder(pass, fn, (*ast.SwitchStmt)(nil)) return nil, nil } var ( checkYodaConditionsQ = pattern.MustParse(`(BinaryExpr left@(TrulyConstantExpression _) tok@(Or "==" "!=") right@(Not (TrulyConstantExpression _)))`) checkYodaConditionsR = pattern.MustParse(`(BinaryExpr right tok left)`) ) func CheckYodaConditions(pass *analysis.Pass) (interface{}, error) { fn := func(node ast.Node) { if _, edits, ok := code.MatchAndEdit(pass, checkYodaConditionsQ, checkYodaConditionsR, node); ok { report.Report(pass, node, "don't use Yoda conditions", report.FilterGenerated(), report.Fixes(edit.Fix("un-Yoda-fy", edits...))) } } code.Preorder(pass, fn, (*ast.BinaryExpr)(nil)) return nil, nil } func CheckInvisibleCharacters(pass *analysis.Pass) (interface{}, error) { fn := func(node ast.Node) { lit := node.(*ast.BasicLit) if lit.Kind != token.STRING { return } type invalid struct { r rune off int } var invalids []invalid hasFormat := false hasControl := false prev := rune(-1) const zwj = '\u200d' for off, r := range lit.Value { if unicode.Is(unicode.Cf, r) { // Don't flag joined emojis. These are multiple emojis joined with ZWJ, which some platform render as single composite emojis. // For the purpose of this check, we consider all symbols, including all symbol modifiers, emoji. if r != zwj || (r == zwj && !unicode.Is(unicode.S, prev)) { invalids = append(invalids, invalid{r, off}) hasFormat = true } } else if unicode.Is(unicode.Cc, r) && r != '\n' && r != '\t' && r != '\r' { invalids = append(invalids, invalid{r, off}) hasControl = true } prev = r } switch len(invalids) { case 0: return case 1: var kind string if hasFormat { kind = "format" } else if hasControl { kind = "control" } else { panic("unreachable") } r := invalids[0] msg := fmt.Sprintf("string literal contains the Unicode %s character %U, consider using the %q escape sequence instead", kind, r.r, r.r) replacement := strconv.QuoteRune(r.r) replacement = replacement[1 : len(replacement)-1] edit := analysis.SuggestedFix{ Message: fmt.Sprintf("replace %s character %U with %q", kind, r.r, r.r), TextEdits: []analysis.TextEdit{{ Pos: lit.Pos() + token.Pos(r.off), End: lit.Pos() + token.Pos(r.off) + token.Pos(utf8.RuneLen(r.r)), NewText: []byte(replacement), }}, } delete := analysis.SuggestedFix{ Message: fmt.Sprintf("delete %s character %U", kind, r.r), TextEdits: []analysis.TextEdit{{ Pos: lit.Pos() + token.Pos(r.off), End: lit.Pos() + token.Pos(r.off) + token.Pos(utf8.RuneLen(r.r)), }}, } report.Report(pass, lit, msg, report.Fixes(edit, delete)) default: var kind string if hasFormat && hasControl { kind = "format and control" } else if hasFormat { kind = "format" } else if hasControl { kind = "control" } else { panic("unreachable") } msg := fmt.Sprintf("string literal contains Unicode %s characters, consider using escape sequences instead", kind) var edits []analysis.TextEdit var deletions []analysis.TextEdit for _, r := range invalids { replacement := strconv.QuoteRune(r.r) replacement = replacement[1 : len(replacement)-1] edits = append(edits, analysis.TextEdit{ Pos: lit.Pos() + token.Pos(r.off), End: lit.Pos() + token.Pos(r.off) + token.Pos(utf8.RuneLen(r.r)), NewText: []byte(replacement), }) deletions = append(deletions, analysis.TextEdit{ Pos: lit.Pos() + token.Pos(r.off), End: lit.Pos() + token.Pos(r.off) + token.Pos(utf8.RuneLen(r.r)), }) } edit := analysis.SuggestedFix{ Message: fmt.Sprintf("replace all %s characters with escape sequences", kind), TextEdits: edits, } delete := analysis.SuggestedFix{ Message: fmt.Sprintf("delete all %s characters", kind), TextEdits: deletions, } report.Report(pass, lit, msg, report.Fixes(edit, delete)) } } code.Preorder(pass, fn, (*ast.BasicLit)(nil)) return nil, nil } func CheckExportedFunctionDocs(pass *analysis.Pass) (interface{}, error) { fn := func(node ast.Node) { if code.IsInTest(pass, node) { return } decl := node.(*ast.FuncDecl) text, ok := docText(decl.Doc) if !ok { return } if !ast.IsExported(decl.Name.Name) { return } kind := "function" if decl.Recv != nil { kind = "method" var ident *ast.Ident T := decl.Recv.List[0].Type if T_, ok := T.(*ast.StarExpr); ok { T = T_.X } switch T := T.(type) { case *ast.IndexExpr: ident = T.X.(*ast.Ident) case *typeparams.IndexListExpr: ident = T.X.(*ast.Ident) case *ast.Ident: ident = T default: lint.ExhaustiveTypeSwitch(T) } if !ast.IsExported(ident.Name) { return } } prefix := decl.Name.Name + " " if !strings.HasPrefix(text, prefix) { 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()) } } code.Preorder(pass, fn, (*ast.FuncDecl)(nil)) return nil, nil } func CheckExportedTypeDocs(pass *analysis.Pass) (interface{}, error) { var genDecl *ast.GenDecl fn := func(node ast.Node, push bool) bool { if !push { genDecl = nil return false } if code.IsInTest(pass, node) { return false } switch node := node.(type) { case *ast.GenDecl: if node.Tok == token.IMPORT { return false } genDecl = node return true case *ast.TypeSpec: if !ast.IsExported(node.Name.Name) { return false } doc := node.Doc text, ok := docText(doc) if !ok { if len(genDecl.Specs) != 1 { // more than one spec in the GenDecl, don't validate the // docstring return false } if genDecl.Lparen.IsValid() { // 'type ( T )' is weird, don't guess the user's intention return false } doc = genDecl.Doc text, ok = docText(doc) if !ok { return false } } // Check comment before we strip articles in case the type's name is an article. if strings.HasPrefix(text, node.Name.Name+" ") { return false } s := text articles := [...]string{"A", "An", "The"} for _, a := range articles { if strings.HasPrefix(s, a+" ") { s = s[len(a)+1:] break } } if !strings.HasPrefix(s, node.Name.Name+" ") { 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()) } return false case *ast.FuncLit, *ast.FuncDecl: return false default: lint.ExhaustiveTypeSwitch(node) return false } } pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Nodes([]ast.Node{(*ast.GenDecl)(nil), (*ast.TypeSpec)(nil), (*ast.FuncLit)(nil), (*ast.FuncDecl)(nil)}, fn) return nil, nil } func CheckExportedVarDocs(pass *analysis.Pass) (interface{}, error) { var genDecl *ast.GenDecl fn := func(node ast.Node, push bool) bool { if !push { genDecl = nil return false } if code.IsInTest(pass, node) { return false } switch node := node.(type) { case *ast.GenDecl: if node.Tok == token.IMPORT { return false } genDecl = node return true case *ast.ValueSpec: if genDecl.Lparen.IsValid() || len(node.Names) > 1 { // Don't try to guess the user's intention return false } name := node.Names[0].Name if !ast.IsExported(name) { return false } text, ok := docText(genDecl.Doc) if !ok { return false } prefix := name + " " if !strings.HasPrefix(text, prefix) { kind := "var" if genDecl.Tok == token.CONST { kind = "const" } report.Report(pass, genDecl.Doc, fmt.Sprintf(`comment on exported %s %s should be of the form "%s..."`, kind, name, prefix), report.FilterGenerated()) } return false case *ast.FuncLit, *ast.FuncDecl: return false default: lint.ExhaustiveTypeSwitch(node) return false } } pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Nodes([]ast.Node{(*ast.GenDecl)(nil), (*ast.ValueSpec)(nil), (*ast.FuncLit)(nil), (*ast.FuncDecl)(nil)}, fn) return nil, nil }