435c0d9fc8
This PR switches the Nomad repository from using govendor to Go modules for managing dependencies. Aspects of the Nomad workflow remain pretty much the same. The usual Makefile targets should continue to work as they always did. The API submodule simply defers to the parent Nomad version on the repository, keeping the semantics of API versioning that currently exists.
915 lines
25 KiB
Go
915 lines
25 KiB
Go
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
|
|
}
|