X-Git-Url: https://git.josue.xyz/?a=blobdiff_plain;f=.config%2Fcoc%2Fextensions%2Fcoc-go-data%2Ftools%2Fpkg%2Fmod%2Fhonnef.co%2Fgo%2Ftools%40v0.0.1-2020.1.5%2Fstylecheck%2Flint.go;fp=.config%2Fcoc%2Fextensions%2Fcoc-go-data%2Ftools%2Fpkg%2Fmod%2Fhonnef.co%2Fgo%2Ftools%40v0.0.1-2020.1.5%2Fstylecheck%2Flint.go;h=75a0112b2364f77bf08a98281c2e48d35065dc4d;hb=4d07c77cf4d78cab8639e13ddc3c22495e585b0b;hp=0000000000000000000000000000000000000000;hpb=b3950616b54221c40a7dab9099bda675007e5b6e;p=dotfiles%2F.git diff --git a/.config/coc/extensions/coc-go-data/tools/pkg/mod/honnef.co/go/tools@v0.0.1-2020.1.5/stylecheck/lint.go b/.config/coc/extensions/coc-go-data/tools/pkg/mod/honnef.co/go/tools@v0.0.1-2020.1.5/stylecheck/lint.go new file mode 100644 index 00000000..75a0112b --- /dev/null +++ b/.config/coc/extensions/coc-go-data/tools/pkg/mod/honnef.co/go/tools@v0.0.1-2020.1.5/stylecheck/lint.go @@ -0,0 +1,914 @@ +package stylecheck // import "honnef.co/go/tools/stylecheck" + +import ( + "fmt" + "go/ast" + "go/constant" + "go/token" + "go/types" + "sort" + "strconv" + "strings" + "unicode" + "unicode/utf8" + + "honnef.co/go/tools/code" + "honnef.co/go/tools/config" + "honnef.co/go/tools/edit" + "honnef.co/go/tools/internal/passes/buildir" + "honnef.co/go/tools/ir" + . "honnef.co/go/tools/lint/lintdsl" + "honnef.co/go/tools/pattern" + "honnef.co/go/tools/report" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/go/types/typeutil" +) + +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 + } + if f.Doc != nil && len(f.Doc.List) > 0 { + hasDocs = true + prefix := "Package " + f.Name.Name + " " + if !strings.HasPrefix(strings.TrimSpace(f.Doc.Text()), prefix) { + report.Report(pass, f.Doc, fmt.Sprintf(`package comment should be of the form "%s..."`, prefix)) + } + f.Doc.Text() + } + } + + 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 !code.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 && code.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 +} + +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) { + assign := node.(*ast.AssignStmt) + if assign.Tok != token.ADD_ASSIGN && assign.Tok != token.SUB_ASSIGN { + return + } + if (len(assign.Lhs) != 1 || len(assign.Rhs) != 1) || + !code.IsIntLiteral(assign.Rhs[0], "1") { + return + } + + suffix := "" + switch assign.Tok { + case token.ADD_ASSIGN: + suffix = "++" + case token.SUB_ASSIGN: + suffix = "--" + } + + report.Report(pass, assign, fmt.Sprintf("should replace %s with %s%s", report.Render(pass, assign), report.Render(pass, assign.Lhs[0]), 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 + } + 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 != "" || 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(code.Dereference(sig.Recv().Type()).(*types.Named).Obj().Name()) { + continue + } + res := sig.Results() + for i := 0; i < res.Len(); i++ { + if named, ok := code.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 code.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 code.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 != "" || 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 !code.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 a newline") + } + 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 + } + } + + 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 !code.IsType(T, "time.Duration") && !code.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.IsCallToAnyAST(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[int]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.CallNameAST(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 + } + lit, ok := call.Args[arg].(*ast.BasicLit) + if !ok { + return + } + if whitelist[lit.Value] { + return + } + + n, err := strconv.Atoi(lit.Value) + if err != nil { + return + } + s, ok := httpStatusCodes[n] + if !ok { + return + } + 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(pass.Fset, 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@(BasicLit _ _) tok@(Or "==" "!=") right@(Not (BasicLit _ _)))`) + checkYodaConditionsR = pattern.MustParse(`(BinaryExpr right tok left)`) +) + +func CheckYodaConditions(pass *analysis.Pass) (interface{}, error) { + fn := func(node ast.Node) { + if _, edits, ok := 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 + for off, r := range lit.Value { + if unicode.Is(unicode.Cf, r) { + 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 + } + } + + 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), + 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) + if decl.Doc == nil { + return + } + if !ast.IsExported(decl.Name.Name) { + return + } + kind := "function" + if decl.Recv != nil { + kind = "method" + switch T := decl.Recv.List[0].Type.(type) { + case *ast.StarExpr: + if !ast.IsExported(T.X.(*ast.Ident).Name) { + return + } + case *ast.Ident: + if !ast.IsExported(T.Name) { + return + } + default: + ExhaustiveTypeSwitch(T) + } + } + prefix := decl.Name.Name + " " + if !strings.HasPrefix(decl.Doc.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 + if doc == nil { + 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 + if doc == nil { + return false + } + } + + s := doc.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: + 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 + } + if genDecl.Doc == nil { + return false + } + prefix := name + " " + if !strings.HasPrefix(genDecl.Doc.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: + 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 +}