Giant blob of minor changes
[dotfiles/.git] / .config / coc / extensions / coc-go-data / tools / pkg / mod / honnef.co / go / tools@v0.0.1-2020.1.5 / stylecheck / lint.go
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 (file)
index 0000000..75a0112
--- /dev/null
@@ -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
+}