mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-12-01 22:31:15 +00:00
c28f7cb29f
Initial part of #435
936 lines
25 KiB
Go
936 lines
25 KiB
Go
package stylecheck
|
|
|
|
import (
|
|
"fmt"
|
|
"go/ast"
|
|
"go/constant"
|
|
"go/token"
|
|
"go/types"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"honnef.co/go/tools/analysis/code"
|
|
"honnef.co/go/tools/analysis/edit"
|
|
"honnef.co/go/tools/analysis/lint"
|
|
"honnef.co/go/tools/analysis/report"
|
|
"honnef.co/go/tools/config"
|
|
"honnef.co/go/tools/go/ast/astutil"
|
|
"honnef.co/go/tools/go/ir"
|
|
"honnef.co/go/tools/go/ir/irutil"
|
|
"honnef.co/go/tools/go/types/typeutil"
|
|
"honnef.co/go/tools/internal/passes/buildir"
|
|
"honnef.co/go/tools/pattern"
|
|
|
|
"golang.org/x/tools/go/analysis"
|
|
"golang.org/x/tools/go/analysis/passes/inspect"
|
|
"golang.org/x/tools/go/ast/inspector"
|
|
)
|
|
|
|
func docText(doc *ast.CommentGroup) (string, bool) {
|
|
if doc == nil {
|
|
return "", false
|
|
}
|
|
// We trim spaces primarily because of /**/ style comments, which often have leading space.
|
|
text := strings.TrimSpace(doc.Text())
|
|
return text, text != ""
|
|
}
|
|
|
|
func CheckPackageComment(pass *analysis.Pass) (interface{}, error) {
|
|
// - At least one file in a non-main package should have a package comment
|
|
//
|
|
// - The comment should be of the form
|
|
// "Package x ...". This has a slight potential for false
|
|
// positives, as multiple files can have package comments, in
|
|
// which case they get appended. But that doesn't happen a lot in
|
|
// the real world.
|
|
|
|
if pass.Pkg.Name() == "main" {
|
|
return nil, nil
|
|
}
|
|
hasDocs := false
|
|
for _, f := range pass.Files {
|
|
if code.IsInTest(pass, f) {
|
|
continue
|
|
}
|
|
text, ok := docText(f.Doc)
|
|
if ok {
|
|
hasDocs = true
|
|
prefix := "Package " + f.Name.Name + " "
|
|
if !strings.HasPrefix(text, prefix) {
|
|
report.Report(pass, f.Doc, fmt.Sprintf(`package comment should be of the form "%s..."`, prefix))
|
|
}
|
|
}
|
|
}
|
|
|
|
if !hasDocs {
|
|
for _, f := range pass.Files {
|
|
if code.IsInTest(pass, f) {
|
|
continue
|
|
}
|
|
report.Report(pass, f, "at least one file in a package should have a package comment", report.ShortRange())
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func CheckDotImports(pass *analysis.Pass) (interface{}, error) {
|
|
for _, f := range pass.Files {
|
|
imports:
|
|
for _, imp := range f.Imports {
|
|
path := imp.Path.Value
|
|
path = path[1 : len(path)-1]
|
|
for _, w := range config.For(pass).DotImportWhitelist {
|
|
if w == path {
|
|
continue imports
|
|
}
|
|
}
|
|
|
|
if imp.Name != nil && imp.Name.Name == "." && !code.IsInTest(pass, f) {
|
|
report.Report(pass, imp, "should not use dot imports", report.FilterGenerated())
|
|
}
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func CheckDuplicatedImports(pass *analysis.Pass) (interface{}, error) {
|
|
for _, f := range pass.Files {
|
|
// Collect all imports by their import path
|
|
imports := make(map[string][]*ast.ImportSpec, len(f.Imports))
|
|
for _, imp := range f.Imports {
|
|
imports[imp.Path.Value] = append(imports[imp.Path.Value], imp)
|
|
}
|
|
|
|
for path, value := range imports {
|
|
if path[1:len(path)-1] == "unsafe" {
|
|
// Don't flag unsafe. Cgo generated code imports
|
|
// unsafe using the blank identifier, and most
|
|
// user-written cgo code also imports unsafe
|
|
// explicitly.
|
|
continue
|
|
}
|
|
// If there's more than one import per path, we flag that
|
|
if len(value) > 1 {
|
|
s := fmt.Sprintf("package %s is being imported more than once", path)
|
|
opts := []report.Option{report.FilterGenerated()}
|
|
for _, imp := range value[1:] {
|
|
opts = append(opts, report.Related(imp, fmt.Sprintf("other import of %s", path)))
|
|
}
|
|
report.Report(pass, value[0], s, opts...)
|
|
}
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func CheckBlankImports(pass *analysis.Pass) (interface{}, error) {
|
|
fset := pass.Fset
|
|
for _, f := range pass.Files {
|
|
if code.IsMainLike(pass) || code.IsInTest(pass, f) {
|
|
continue
|
|
}
|
|
|
|
// Collect imports of the form `import _ "foo"`, i.e. with no
|
|
// parentheses, as their comment will be associated with the
|
|
// (paren-free) GenDecl, not the import spec itself.
|
|
//
|
|
// We don't directly process the GenDecl so that we can
|
|
// correctly handle the following:
|
|
//
|
|
// import _ "foo"
|
|
// import _ "bar"
|
|
//
|
|
// where only the first import should get flagged.
|
|
skip := map[ast.Spec]bool{}
|
|
ast.Inspect(f, func(node ast.Node) bool {
|
|
switch node := node.(type) {
|
|
case *ast.File:
|
|
return true
|
|
case *ast.GenDecl:
|
|
if node.Tok != token.IMPORT {
|
|
return false
|
|
}
|
|
if node.Lparen == token.NoPos && node.Doc != nil {
|
|
skip[node.Specs[0]] = true
|
|
}
|
|
return false
|
|
}
|
|
return false
|
|
})
|
|
for i, imp := range f.Imports {
|
|
pos := fset.Position(imp.Pos())
|
|
|
|
if !astutil.IsBlank(imp.Name) {
|
|
continue
|
|
}
|
|
// Only flag the first blank import in a group of imports,
|
|
// or don't flag any of them, if the first one is
|
|
// commented
|
|
if i > 0 {
|
|
prev := f.Imports[i-1]
|
|
prevPos := fset.Position(prev.Pos())
|
|
if pos.Line-1 == prevPos.Line && astutil.IsBlank(prev.Name) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if imp.Doc == nil && imp.Comment == nil && !skip[imp] {
|
|
report.Report(pass, imp, "a blank import should be only in a main or test package, or have a comment justifying it")
|
|
}
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
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) ||
|
|
!astutil.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 != 0 || fn.Parent() != nil {
|
|
continue
|
|
}
|
|
if !ast.IsExported(fn.Name()) || code.IsMain(pass) || code.IsInTest(pass, fn) {
|
|
continue
|
|
}
|
|
sig := fn.Type().(*types.Signature)
|
|
if sig.Recv() != nil && !ast.IsExported(typeutil.Dereference(sig.Recv().Type()).(*types.Named).Obj().Name()) {
|
|
continue
|
|
}
|
|
res := sig.Results()
|
|
for i := 0; i < res.Len(); i++ {
|
|
if named, ok := typeutil.DereferenceR(res.At(i).Type()).(*types.Named); ok &&
|
|
!ast.IsExported(named.Obj().Name()) &&
|
|
named != types.Universe.Lookup("error").Type() {
|
|
report.Report(pass, fn, "should not return unexported type")
|
|
}
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func CheckReceiverNames(pass *analysis.Pass) (interface{}, error) {
|
|
irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg
|
|
for _, m := range irpkg.Members {
|
|
if T, ok := m.Object().(*types.TypeName); ok && !T.IsAlias() {
|
|
ms := typeutil.IntuitiveMethodSet(T.Type(), nil)
|
|
for _, sel := range ms {
|
|
fn := sel.Obj().(*types.Func)
|
|
recv := fn.Type().(*types.Signature).Recv()
|
|
if typeutil.Dereference(recv.Type()) != T.Type() {
|
|
// skip embedded methods
|
|
continue
|
|
}
|
|
if recv.Name() == "self" || recv.Name() == "this" {
|
|
report.Report(pass, recv, `receiver name should be a reflection of its identity; don't use generic names such as "this" or "self"`, report.FilterGenerated())
|
|
}
|
|
if recv.Name() == "_" {
|
|
report.Report(pass, recv, "receiver name should not be an underscore, omit the name if it is unused", report.FilterGenerated())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func CheckReceiverNamesIdentical(pass *analysis.Pass) (interface{}, error) {
|
|
irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg
|
|
for _, m := range irpkg.Members {
|
|
names := map[string]int{}
|
|
|
|
var firstFn *types.Func
|
|
if T, ok := m.Object().(*types.TypeName); ok && !T.IsAlias() {
|
|
ms := typeutil.IntuitiveMethodSet(T.Type(), nil)
|
|
for _, sel := range ms {
|
|
fn := sel.Obj().(*types.Func)
|
|
recv := fn.Type().(*types.Signature).Recv()
|
|
if code.IsGenerated(pass, recv.Pos()) {
|
|
// Don't concern ourselves with methods in generated code
|
|
continue
|
|
}
|
|
if typeutil.Dereference(recv.Type()) != T.Type() {
|
|
// skip embedded methods
|
|
continue
|
|
}
|
|
if firstFn == nil {
|
|
firstFn = fn
|
|
}
|
|
if recv.Name() != "" && recv.Name() != "_" {
|
|
names[recv.Name()]++
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(names) > 1 {
|
|
var seen []string
|
|
for name, count := range names {
|
|
seen = append(seen, fmt.Sprintf("%dx %q", count, name))
|
|
}
|
|
sort.Strings(seen)
|
|
|
|
report.Report(pass, firstFn, fmt.Sprintf("methods on the same type should have the same receiver name (seen %s)", strings.Join(seen, ", ")))
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func CheckContextFirstArg(pass *analysis.Pass) (interface{}, error) {
|
|
// TODO(dh): this check doesn't apply to test helpers. Example from the stdlib:
|
|
// func helperCommandContext(t *testing.T, ctx context.Context, s ...string) (cmd *exec.Cmd) {
|
|
fnLoop:
|
|
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
|
|
if fn.Synthetic != 0 || fn.Parent() != nil {
|
|
continue
|
|
}
|
|
params := fn.Signature.Params()
|
|
if params.Len() < 2 {
|
|
continue
|
|
}
|
|
if types.TypeString(params.At(0).Type(), nil) == "context.Context" {
|
|
continue
|
|
}
|
|
for i := 1; i < params.Len(); i++ {
|
|
param := params.At(i)
|
|
if types.TypeString(param.Type(), nil) == "context.Context" {
|
|
report.Report(pass, param, "context.Context should be the first argument of a function", report.ShortRange())
|
|
continue fnLoop
|
|
}
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func CheckErrorStrings(pass *analysis.Pass) (interface{}, error) {
|
|
objNames := map[*ir.Package]map[string]bool{}
|
|
irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg
|
|
objNames[irpkg] = map[string]bool{}
|
|
for _, m := range irpkg.Members {
|
|
if typ, ok := m.(*ir.Type); ok {
|
|
objNames[irpkg][typ.Name()] = true
|
|
}
|
|
}
|
|
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
|
|
objNames[fn.Package()][fn.Name()] = true
|
|
}
|
|
|
|
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
|
|
if code.IsInTest(pass, fn) {
|
|
// We don't care about malformed error messages in tests;
|
|
// they're usually for direct human consumption, not part
|
|
// of an API
|
|
continue
|
|
}
|
|
for _, block := range fn.Blocks {
|
|
instrLoop:
|
|
for _, ins := range block.Instrs {
|
|
call, ok := ins.(*ir.Call)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if !irutil.IsCallToAny(call.Common(), "errors.New", "fmt.Errorf") {
|
|
continue
|
|
}
|
|
|
|
k, ok := call.Common().Args[0].(*ir.Const)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
s := constant.StringVal(k.Value)
|
|
if len(s) == 0 {
|
|
continue
|
|
}
|
|
switch s[len(s)-1] {
|
|
case '.', ':', '!', '\n':
|
|
report.Report(pass, call, "error strings should not end with punctuation or 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
|
|
}
|
|
}
|
|
|
|
if strings.ContainsRune(word, '(') {
|
|
// Might be a function call
|
|
continue instrLoop
|
|
}
|
|
word = strings.TrimRightFunc(word, func(r rune) bool { return unicode.IsPunct(r) })
|
|
if objNames[fn.Package()][word] {
|
|
// Word is probably the name of a function or type in this package
|
|
continue
|
|
}
|
|
// First word in error starts with a capital
|
|
// letter, and the word doesn't contain any other
|
|
// capitals, making it unlikely to be an
|
|
// initialism or multi-word function name.
|
|
//
|
|
// It could still be a proper noun, though.
|
|
|
|
report.Report(pass, call, "error strings should not be capitalized")
|
|
}
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func CheckTimeNames(pass *analysis.Pass) (interface{}, error) {
|
|
suffixes := []string{
|
|
"Sec", "Secs", "Seconds",
|
|
"Msec", "Msecs",
|
|
"Milli", "Millis", "Milliseconds",
|
|
"Usec", "Usecs", "Microseconds",
|
|
"MS", "Ms",
|
|
}
|
|
fn := func(names []*ast.Ident) {
|
|
for _, name := range names {
|
|
if _, ok := pass.TypesInfo.Defs[name]; !ok {
|
|
continue
|
|
}
|
|
T := pass.TypesInfo.TypeOf(name)
|
|
if !typeutil.IsType(T, "time.Duration") && !typeutil.IsType(T, "*time.Duration") {
|
|
continue
|
|
}
|
|
for _, suffix := range suffixes {
|
|
if strings.HasSuffix(name.Name, suffix) {
|
|
report.Report(pass, name, fmt.Sprintf("var %s is of type %v; don't use unit-specific suffix %q", name.Name, T, suffix))
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn2 := func(node ast.Node) {
|
|
switch node := node.(type) {
|
|
case *ast.ValueSpec:
|
|
fn(node.Names)
|
|
case *ast.FieldList:
|
|
for _, field := range node.List {
|
|
fn(field.Names)
|
|
}
|
|
case *ast.AssignStmt:
|
|
if node.Tok != token.DEFINE {
|
|
break
|
|
}
|
|
var names []*ast.Ident
|
|
for _, lhs := range node.Lhs {
|
|
if lhs, ok := lhs.(*ast.Ident); ok {
|
|
names = append(names, lhs)
|
|
}
|
|
}
|
|
fn(names)
|
|
}
|
|
}
|
|
|
|
code.Preorder(pass, fn2, (*ast.ValueSpec)(nil), (*ast.FieldList)(nil), (*ast.AssignStmt)(nil))
|
|
return nil, nil
|
|
}
|
|
|
|
func CheckErrorVarNames(pass *analysis.Pass) (interface{}, error) {
|
|
for _, f := range pass.Files {
|
|
for _, decl := range f.Decls {
|
|
gen, ok := decl.(*ast.GenDecl)
|
|
if !ok || gen.Tok != token.VAR {
|
|
continue
|
|
}
|
|
for _, spec := range gen.Specs {
|
|
spec := spec.(*ast.ValueSpec)
|
|
if len(spec.Names) != len(spec.Values) {
|
|
continue
|
|
}
|
|
|
|
for i, name := range spec.Names {
|
|
val := spec.Values[i]
|
|
if !code.IsCallToAny(pass, val, "errors.New", "fmt.Errorf") {
|
|
continue
|
|
}
|
|
|
|
if pass.Pkg.Path() == "net/http" && strings.HasPrefix(name.Name, "http2err") {
|
|
// special case for internal variable names of
|
|
// bundled HTTP 2 code in net/http
|
|
continue
|
|
}
|
|
prefix := "err"
|
|
if name.IsExported() {
|
|
prefix = "Err"
|
|
}
|
|
if !strings.HasPrefix(name.Name, prefix) {
|
|
report.Report(pass, name, fmt.Sprintf("error var %s should have name of the form %sFoo", name.Name, prefix))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
var httpStatusCodes = map[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.CallName(pass, call) {
|
|
case "net/http.Error":
|
|
arg = 2
|
|
case "net/http.Redirect":
|
|
arg = 3
|
|
case "net/http.StatusText":
|
|
arg = 0
|
|
case "net/http.RedirectHandler":
|
|
arg = 1
|
|
default:
|
|
return
|
|
}
|
|
if arg >= len(call.Args) {
|
|
return
|
|
}
|
|
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 := code.MatchAndEdit(pass, checkYodaConditionsQ, checkYodaConditionsR, node); ok {
|
|
report.Report(pass, node, "don't use Yoda conditions",
|
|
report.FilterGenerated(),
|
|
report.Fixes(edit.Fix("un-Yoda-fy", edits...)))
|
|
}
|
|
}
|
|
code.Preorder(pass, fn, (*ast.BinaryExpr)(nil))
|
|
return nil, nil
|
|
}
|
|
|
|
func CheckInvisibleCharacters(pass *analysis.Pass) (interface{}, error) {
|
|
fn := func(node ast.Node) {
|
|
lit := node.(*ast.BasicLit)
|
|
if lit.Kind != token.STRING {
|
|
return
|
|
}
|
|
|
|
type invalid struct {
|
|
r rune
|
|
off int
|
|
}
|
|
var invalids []invalid
|
|
hasFormat := false
|
|
hasControl := false
|
|
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)
|
|
text, ok := docText(decl.Doc)
|
|
if !ok {
|
|
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:
|
|
lint.ExhaustiveTypeSwitch(T)
|
|
}
|
|
}
|
|
prefix := decl.Name.Name + " "
|
|
if !strings.HasPrefix(text, prefix) {
|
|
report.Report(pass, decl.Doc, fmt.Sprintf(`comment on exported %s %s should be of the form "%s..."`, kind, decl.Name.Name, prefix), report.FilterGenerated())
|
|
}
|
|
}
|
|
|
|
code.Preorder(pass, fn, (*ast.FuncDecl)(nil))
|
|
return nil, nil
|
|
}
|
|
|
|
func CheckExportedTypeDocs(pass *analysis.Pass) (interface{}, error) {
|
|
var genDecl *ast.GenDecl
|
|
fn := func(node ast.Node, push bool) bool {
|
|
if !push {
|
|
genDecl = nil
|
|
return false
|
|
}
|
|
if code.IsInTest(pass, node) {
|
|
return false
|
|
}
|
|
|
|
switch node := node.(type) {
|
|
case *ast.GenDecl:
|
|
if node.Tok == token.IMPORT {
|
|
return false
|
|
}
|
|
genDecl = node
|
|
return true
|
|
case *ast.TypeSpec:
|
|
if !ast.IsExported(node.Name.Name) {
|
|
return false
|
|
}
|
|
|
|
doc := node.Doc
|
|
text, ok := docText(doc)
|
|
if !ok {
|
|
if len(genDecl.Specs) != 1 {
|
|
// more than one spec in the GenDecl, don't validate the
|
|
// docstring
|
|
return false
|
|
}
|
|
if genDecl.Lparen.IsValid() {
|
|
// 'type ( T )' is weird, don't guess the user's intention
|
|
return false
|
|
}
|
|
doc = genDecl.Doc
|
|
text, ok = docText(doc)
|
|
if !ok {
|
|
return false
|
|
}
|
|
}
|
|
|
|
s := text
|
|
articles := [...]string{"A", "An", "The"}
|
|
for _, a := range articles {
|
|
if strings.HasPrefix(s, a+" ") {
|
|
s = s[len(a)+1:]
|
|
break
|
|
}
|
|
}
|
|
if !strings.HasPrefix(s, node.Name.Name+" ") {
|
|
report.Report(pass, doc, fmt.Sprintf(`comment on exported type %s should be of the form "%s ..." (with optional leading article)`, node.Name.Name, node.Name.Name), report.FilterGenerated())
|
|
}
|
|
return false
|
|
case *ast.FuncLit, *ast.FuncDecl:
|
|
return false
|
|
default:
|
|
lint.ExhaustiveTypeSwitch(node)
|
|
return false
|
|
}
|
|
}
|
|
|
|
pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Nodes([]ast.Node{(*ast.GenDecl)(nil), (*ast.TypeSpec)(nil), (*ast.FuncLit)(nil), (*ast.FuncDecl)(nil)}, fn)
|
|
return nil, nil
|
|
}
|
|
|
|
func CheckExportedVarDocs(pass *analysis.Pass) (interface{}, error) {
|
|
var genDecl *ast.GenDecl
|
|
fn := func(node ast.Node, push bool) bool {
|
|
if !push {
|
|
genDecl = nil
|
|
return false
|
|
}
|
|
if code.IsInTest(pass, node) {
|
|
return false
|
|
}
|
|
|
|
switch node := node.(type) {
|
|
case *ast.GenDecl:
|
|
if node.Tok == token.IMPORT {
|
|
return false
|
|
}
|
|
genDecl = node
|
|
return true
|
|
case *ast.ValueSpec:
|
|
if genDecl.Lparen.IsValid() || len(node.Names) > 1 {
|
|
// Don't try to guess the user's intention
|
|
return false
|
|
}
|
|
name := node.Names[0].Name
|
|
if !ast.IsExported(name) {
|
|
return false
|
|
}
|
|
text, ok := docText(genDecl.Doc)
|
|
if !ok {
|
|
return false
|
|
}
|
|
prefix := name + " "
|
|
if !strings.HasPrefix(text, prefix) {
|
|
kind := "var"
|
|
if genDecl.Tok == token.CONST {
|
|
kind = "const"
|
|
}
|
|
report.Report(pass, genDecl.Doc, fmt.Sprintf(`comment on exported %s %s should be of the form "%s..."`, kind, name, prefix), report.FilterGenerated())
|
|
}
|
|
return false
|
|
case *ast.FuncLit, *ast.FuncDecl:
|
|
return false
|
|
default:
|
|
lint.ExhaustiveTypeSwitch(node)
|
|
return false
|
|
}
|
|
}
|
|
|
|
pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Nodes([]ast.Node{(*ast.GenDecl)(nil), (*ast.ValueSpec)(nil), (*ast.FuncLit)(nil), (*ast.FuncDecl)(nil)}, fn)
|
|
return nil, nil
|
|
}
|