woodpecker/vendor/github.com/blizzy78/varnamelen/varnamelen.go
2021-11-16 21:07:53 +01:00

342 lines
9.1 KiB
Go

package varnamelen
import (
"go/ast"
"strings"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
)
// varNameLen is an analyzer that checks that the length of a variable's name matches its usage scope.
// It will create a report for a variable's assignment if that variable has a short name, but its
// usage scope is not considered "small."
type varNameLen struct {
// maxDistance is the longest distance, in source lines, that is being considered a "small scope."
maxDistance int
// minNameLength is the minimum length of a variable's name that is considered "long."
minNameLength int
// ignoreNames is an optional list of variable names that should be ignored completely.
ignoreNames stringsValue
// checkReceiver determines whether a method receiver's name should be checked.
checkReceiver bool
// checkReturn determines whether named return values should be checked.
checkReturn bool
}
// stringsValue is the value of a list-of-strings flag.
type stringsValue struct {
Values []string
}
// variable represents a declared variable.
type variable struct {
// name is the name of the variable.
name string
// assign is the assign statement that declares the variable.
assign *ast.AssignStmt
}
// parameter represents a declared function or method parameter.
type parameter struct {
// name is the name of the parameter.
name string
// field is the declaration of the parameter.
field *ast.Field
}
const (
// defaultMaxDistance is the default value for the maximum distance between the declaration of a variable and its usage
// that is considered a "small scope."
defaultMaxDistance = 5
// defaultMinNameLength is the default value for the minimum length of a variable's name that is considered "long."
defaultMinNameLength = 3
)
// NewAnalyzer returns a new analyzer that checks variable name length.
func NewAnalyzer() *analysis.Analyzer {
vnl := varNameLen{
maxDistance: defaultMaxDistance,
minNameLength: defaultMinNameLength,
ignoreNames: stringsValue{},
}
analyzer := analysis.Analyzer{
Name: "varnamelen",
Doc: "checks that the length of a variable's name matches its scope\n\n" +
"A variable with a short name can be hard to use if the variable is used\n" +
"over a longer span of lines of code. A longer variable name may be easier\n" +
"to comprehend.",
Run: func(pass *analysis.Pass) (interface{}, error) {
vnl.run(pass)
return nil, nil
},
Requires: []*analysis.Analyzer{
inspect.Analyzer,
},
}
analyzer.Flags.IntVar(&vnl.maxDistance, "maxDistance", defaultMaxDistance, "maximum number of lines of variable usage scope considered 'short'")
analyzer.Flags.IntVar(&vnl.minNameLength, "minNameLength", defaultMinNameLength, "minimum length of variable name considered 'long'")
analyzer.Flags.Var(&vnl.ignoreNames, "ignoreNames", "comma-separated list of ignored variable names")
analyzer.Flags.BoolVar(&vnl.checkReceiver, "checkReceiver", false, "check method receiver names")
analyzer.Flags.BoolVar(&vnl.checkReturn, "checkReturn", false, "check named return values")
return &analyzer
}
// Run applies v to a package, according to pass.
func (v *varNameLen) run(pass *analysis.Pass) {
varToDist, paramToDist, returnToDist := v.distances(pass)
for variable, dist := range varToDist {
if v.checkNameAndDistance(variable.name, dist) {
continue
}
pass.Reportf(variable.assign.Pos(), "variable name '%s' is too short for the scope of its usage", variable.name)
}
for param, dist := range paramToDist {
if param.isConventional() {
continue
}
if v.checkNameAndDistance(param.name, dist) {
continue
}
pass.Reportf(param.field.Pos(), "parameter name '%s' is too short for the scope of its usage", param.name)
}
for param, dist := range returnToDist {
if v.checkNameAndDistance(param.name, dist) {
continue
}
pass.Reportf(param.field.Pos(), "return value name '%s' is too short for the scope of its usage", param.name)
}
}
// checkNameAndDistance returns true when name or dist are considered "short", or when name is to be ignored.
func (v *varNameLen) checkNameAndDistance(name string, dist int) bool {
if len(name) >= v.minNameLength {
return true
}
if dist <= v.maxDistance {
return true
}
if v.ignoreNames.contains(name) {
return true
}
return false
}
// distances maps of variables or parameters and their longest usage distances.
func (v *varNameLen) distances(pass *analysis.Pass) (map[variable]int, map[parameter]int, map[parameter]int) {
assignIdents, paramIdents, returnIdents := v.idents(pass)
varToDist := map[variable]int{}
for _, ident := range assignIdents {
assign := ident.Obj.Decl.(*ast.AssignStmt)
variable := variable{
name: ident.Name,
assign: assign,
}
useLine := pass.Fset.Position(ident.NamePos).Line
declLine := pass.Fset.Position(assign.Pos()).Line
varToDist[variable] = useLine - declLine
}
paramToDist := map[parameter]int{}
for _, ident := range paramIdents {
field := ident.Obj.Decl.(*ast.Field)
param := parameter{
name: ident.Name,
field: field,
}
useLine := pass.Fset.Position(ident.NamePos).Line
declLine := pass.Fset.Position(field.Pos()).Line
paramToDist[param] = useLine - declLine
}
returnToDist := map[parameter]int{}
for _, ident := range returnIdents {
field := ident.Obj.Decl.(*ast.Field)
param := parameter{
name: ident.Name,
field: field,
}
useLine := pass.Fset.Position(ident.NamePos).Line
declLine := pass.Fset.Position(field.Pos()).Line
returnToDist[param] = useLine - declLine
}
return varToDist, paramToDist, returnToDist
}
// idents returns Idents referencing assign statements, parameters, and return values, respectively.
func (v *varNameLen) idents(pass *analysis.Pass) ([]*ast.Ident, []*ast.Ident, []*ast.Ident) { //nolint:gocognit
inspector := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
filter := []ast.Node{
(*ast.Ident)(nil),
(*ast.FuncDecl)(nil),
}
funcs := []*ast.FuncDecl{}
methods := []*ast.FuncDecl{}
assignIdents := []*ast.Ident{}
paramIdents := []*ast.Ident{}
returnIdents := []*ast.Ident{}
inspector.Preorder(filter, func(node ast.Node) {
if f, ok := node.(*ast.FuncDecl); ok {
funcs = append(funcs, f)
if f.Recv != nil {
methods = append(methods, f)
}
return
}
ident := node.(*ast.Ident)
if ident.Obj == nil {
return
}
if _, ok := ident.Obj.Decl.(*ast.AssignStmt); ok {
assignIdents = append(assignIdents, ident)
return
}
if field, ok := ident.Obj.Decl.(*ast.Field); ok {
if isReceiver(field, methods) && !v.checkReceiver {
return
}
if isReturn(field, funcs) {
if !v.checkReturn {
return
}
returnIdents = append(returnIdents, ident)
return
}
paramIdents = append(paramIdents, ident)
}
})
return assignIdents, paramIdents, returnIdents
}
// isReceiver returns true when field is a receiver parameter of any of the given methods.
func isReceiver(field *ast.Field, methods []*ast.FuncDecl) bool {
for _, m := range methods {
for _, recv := range m.Recv.List {
if recv == field {
return true
}
}
}
return false
}
// isReturn returns true when field is a return value of any of the given funcs.
func isReturn(field *ast.Field, funcs []*ast.FuncDecl) bool {
for _, f := range funcs {
if f.Type.Results == nil {
continue
}
for _, r := range f.Type.Results.List {
if r == field {
return true
}
}
}
return false
}
// Set implements Value.
func (sv *stringsValue) Set(s string) error {
sv.Values = strings.Split(s, ",")
return nil
}
// String implements Value.
func (sv *stringsValue) String() string {
return strings.Join(sv.Values, ",")
}
// contains returns true when sv contains s.
func (sv *stringsValue) contains(s string) bool {
for _, v := range sv.Values {
if v == s {
return true
}
}
return false
}
// isConventional returns true when p is a conventional Go parameter, such as "ctx context.Context" or
// "t *testing.T".
func (p parameter) isConventional() bool { //nolint:gocyclo,gocognit
switch {
case p.name == "t" && p.isPointerType("testing.T"):
return true
case p.name == "b" && p.isPointerType("testing.B"):
return true
case p.name == "tb" && p.isType("testing.TB"):
return true
case p.name == "pb" && p.isPointerType("testing.PB"):
return true
case p.name == "m" && p.isPointerType("testing.M"):
return true
case p.name == "ctx" && p.isType("context.Context"):
return true
default:
return false
}
}
// isType returns true when p is of type typeName.
func (p parameter) isType(typeName string) bool {
sel, ok := p.field.Type.(*ast.SelectorExpr)
if !ok {
return false
}
return isType(sel, typeName)
}
// isPointerType returns true when p is a pointer type of type typeName.
func (p parameter) isPointerType(typeName string) bool {
star, ok := p.field.Type.(*ast.StarExpr)
if !ok {
return false
}
sel, ok := star.X.(*ast.SelectorExpr)
if !ok {
return false
}
return isType(sel, typeName)
}
// isType returns true when sel is a selector for type typeName.
func isType(sel *ast.SelectorExpr, typeName string) bool {
ident, ok := sel.X.(*ast.Ident)
if !ok {
return false
}
return typeName == ident.Name+"."+sel.Sel.Name
}