Marvin Preuss
1d4ae27878
All checks were successful
continuous-integration/drone/push Build is passing
135 lines
3.1 KiB
Go
135 lines
3.1 KiB
Go
package analyzer
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/token"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"golang.org/x/tools/go/analysis"
|
|
"golang.org/x/tools/go/analysis/passes/inspect"
|
|
"golang.org/x/tools/go/ast/inspector"
|
|
)
|
|
|
|
// New returns new errname analyzer.
|
|
func New() *analysis.Analyzer {
|
|
return &analysis.Analyzer{
|
|
Name: "errname",
|
|
Doc: "Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`.",
|
|
Run: run,
|
|
Requires: []*analysis.Analyzer{inspect.Analyzer},
|
|
}
|
|
}
|
|
|
|
type stringSet = map[string]struct{}
|
|
|
|
var (
|
|
imports = []ast.Node{(*ast.ImportSpec)(nil)}
|
|
types = []ast.Node{(*ast.TypeSpec)(nil)}
|
|
funcs = []ast.Node{(*ast.FuncDecl)(nil)}
|
|
)
|
|
|
|
func run(pass *analysis.Pass) (interface{}, error) {
|
|
insp := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
|
|
|
|
pkgAliases := map[string]string{}
|
|
insp.Preorder(imports, func(node ast.Node) {
|
|
i := node.(*ast.ImportSpec)
|
|
if n := i.Name; n != nil && i.Path != nil {
|
|
if path, err := strconv.Unquote(i.Path.Value); err == nil {
|
|
pkgAliases[n.Name] = getPkgFromPath(path)
|
|
}
|
|
}
|
|
})
|
|
|
|
allTypes := stringSet{}
|
|
typesSpecs := map[string]*ast.TypeSpec{}
|
|
insp.Preorder(types, func(node ast.Node) {
|
|
t := node.(*ast.TypeSpec)
|
|
allTypes[t.Name.Name] = struct{}{}
|
|
typesSpecs[t.Name.Name] = t
|
|
})
|
|
|
|
errorTypes := stringSet{}
|
|
insp.Preorder(funcs, func(node ast.Node) {
|
|
f := node.(*ast.FuncDecl)
|
|
t, ok := isMethodError(f)
|
|
if !ok {
|
|
return
|
|
}
|
|
errorTypes[t] = struct{}{}
|
|
|
|
tSpec, ok := typesSpecs[t]
|
|
if !ok {
|
|
panic("no specification for type " + t)
|
|
}
|
|
|
|
if _, ok := tSpec.Type.(*ast.ArrayType); ok {
|
|
if !isValidErrorArrayTypeName(t) {
|
|
reportAboutErrorType(pass, tSpec.Pos(), t, true)
|
|
}
|
|
} else if !isValidErrorTypeName(t) {
|
|
reportAboutErrorType(pass, tSpec.Pos(), t, false)
|
|
}
|
|
})
|
|
|
|
errorFuncs := stringSet{}
|
|
insp.Preorder(funcs, func(node ast.Node) {
|
|
f := node.(*ast.FuncDecl)
|
|
if isFuncReturningErr(f.Type, allTypes, errorTypes) {
|
|
errorFuncs[f.Name.Name] = struct{}{}
|
|
}
|
|
})
|
|
|
|
inspectPkgLevelVarsOnly := func(node ast.Node) bool {
|
|
switch v := node.(type) {
|
|
case *ast.FuncDecl:
|
|
return false
|
|
|
|
case *ast.ValueSpec:
|
|
if name, ok := isSentinelError(v, pkgAliases, allTypes, errorTypes, errorFuncs); ok && !isValidErrorVarName(name) {
|
|
reportAboutErrorVar(pass, v.Pos(), name)
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
for _, f := range pass.Files {
|
|
ast.Inspect(f, inspectPkgLevelVarsOnly)
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func reportAboutErrorType(pass *analysis.Pass, typePos token.Pos, typeName string, isArrayType bool) {
|
|
var form string
|
|
if unicode.IsLower([]rune(typeName)[0]) {
|
|
form = "xxxError"
|
|
} else {
|
|
form = "XxxError"
|
|
}
|
|
|
|
if isArrayType {
|
|
form += "s"
|
|
}
|
|
pass.Reportf(typePos, "the type name `%s` should conform to the `%s` format", typeName, form)
|
|
}
|
|
|
|
func reportAboutErrorVar(pass *analysis.Pass, pos token.Pos, varName string) {
|
|
var form string
|
|
if unicode.IsLower([]rune(varName)[0]) {
|
|
form = "errXxx"
|
|
} else {
|
|
form = "ErrXxx"
|
|
}
|
|
pass.Reportf(pos, "the variable name `%s` should conform to the `%s` format", varName, form)
|
|
}
|
|
|
|
func getPkgFromPath(p string) string {
|
|
idx := strings.LastIndex(p, "/")
|
|
if idx == -1 {
|
|
return p
|
|
}
|
|
return p[idx+1:]
|
|
}
|