2021-11-16 20:07:53 +00:00
package varnamelen
import (
"go/ast"
2022-02-24 16:33:24 +00:00
"go/token"
"go/types"
2021-11-16 20:07:53 +00:00
"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
2022-02-24 16:33:24 +00:00
// ignoreTypeAssertOk determines whether "ok" variables that hold the bool return value of a type assertion should be ignored.
ignoreTypeAssertOk bool
// ignoreMapIndexOk determines whether "ok" variables that hold the bool return value of a map index should be ignored.
ignoreMapIndexOk bool
// ignoreChannelReceiveOk determines whether "ok" variables that hold the bool return value of a channel receive should be ignored.
ignoreChannelReceiveOk bool
// ignoreDeclarations is an optional list of variable declarations that should be ignored completely.
ignoreDeclarations declarationsValue
2021-11-16 20:07:53 +00:00
}
// variable represents a declared variable.
type variable struct {
// name is the name of the variable.
name string
2022-02-24 16:33:24 +00:00
// constant is true if the variable is actually a constant.
constant bool
// typ is the type of the variable.
typ string
2021-11-16 20:07:53 +00:00
// assign is the assign statement that declares the variable.
assign * ast . AssignStmt
2022-02-24 16:33:24 +00:00
// valueSpec is the value specification that declares the variable.
valueSpec * ast . ValueSpec
2021-11-16 20:07:53 +00:00
}
// parameter represents a declared function or method parameter.
type parameter struct {
// name is the name of the parameter.
name string
2022-02-24 16:33:24 +00:00
// typ is the type of the parameter.
typ string
2021-11-16 20:07:53 +00:00
// field is the declaration of the parameter.
field * ast . Field
}
2022-02-24 16:33:24 +00:00
// declaration is a variable declaration.
type declaration struct {
// name is the name of the variable.
name string
// constant is true if the variable is actually a constant.
constant bool
// typ is the type of the variable. Not used for constants.
typ string
}
// importDeclaration is an import declaration.
type importDeclaration struct {
// name is the short name or alias for the imported package. This is either the package's default name,
// or the alias specified in the import statement.
// Not used if self is true.
name string
// path is the full path to the imported package.
path string
// self is true when this is an implicit import declaration for the current package.
self bool
}
2021-11-16 20:07:53 +00:00
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
)
2022-02-24 16:33:24 +00:00
// conventionalDecls is a list of conventional variable declarations.
var conventionalDecls = [ ] declaration {
parseDeclaration ( "t *testing.T" ) ,
parseDeclaration ( "b *testing.B" ) ,
parseDeclaration ( "tb testing.TB" ) ,
parseDeclaration ( "pb *testing.PB" ) ,
parseDeclaration ( "m *testing.M" ) ,
parseDeclaration ( "ctx context.Context" ) ,
}
2021-11-16 20:07:53 +00:00
// NewAnalyzer returns a new analyzer that checks variable name length.
func NewAnalyzer ( ) * analysis . Analyzer {
vnl := varNameLen {
2022-02-24 16:33:24 +00:00
maxDistance : defaultMaxDistance ,
minNameLength : defaultMinNameLength ,
ignoreNames : stringsValue { } ,
ignoreDeclarations : declarationsValue { } ,
2021-11-16 20:07:53 +00:00
}
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" )
2022-02-24 16:33:24 +00:00
analyzer . Flags . BoolVar ( & vnl . ignoreTypeAssertOk , "ignoreTypeAssertOk" , false , "ignore 'ok' variables that hold the bool return value of a type assertion" )
analyzer . Flags . BoolVar ( & vnl . ignoreMapIndexOk , "ignoreMapIndexOk" , false , "ignore 'ok' variables that hold the bool return value of a map index" )
analyzer . Flags . BoolVar ( & vnl . ignoreChannelReceiveOk , "ignoreChanRecvOk" , false , "ignore 'ok' variables that hold the bool return value of a channel receive" )
analyzer . Flags . Var ( & vnl . ignoreDeclarations , "ignoreDecls" , "comma-separated list of ignored variable declarations" )
2021-11-16 20:07:53 +00:00
return & analyzer
}
// Run applies v to a package, according to pass.
func ( v * varNameLen ) run ( pass * analysis . Pass ) {
varToDist , paramToDist , returnToDist := v . distances ( pass )
2022-02-24 16:33:24 +00:00
v . checkVariables ( pass , varToDist )
v . checkParams ( pass , paramToDist )
v . checkReturns ( pass , returnToDist )
}
// checkVariables applies v to variables in varToDist.
func ( v * varNameLen ) checkVariables ( pass * analysis . Pass , varToDist map [ variable ] int ) {
2021-11-16 20:07:53 +00:00
for variable , dist := range varToDist {
2022-02-24 16:33:24 +00:00
if v . ignoreNames . contains ( variable . name ) {
continue
}
if v . ignoreDeclarations . matchVariable ( variable ) {
continue
}
2021-11-16 20:07:53 +00:00
if v . checkNameAndDistance ( variable . name , dist ) {
continue
}
2022-02-24 16:33:24 +00:00
if v . checkTypeAssertOk ( variable ) {
continue
}
if v . checkMapIndexOk ( variable ) {
continue
}
if v . checkChannelReceiveOk ( variable ) {
continue
}
if variable . assign != nil {
pass . Reportf ( variable . assign . Pos ( ) , "%s name '%s' is too short for the scope of its usage" , variable . kindName ( ) , variable . name )
continue
}
pass . Reportf ( variable . valueSpec . Pos ( ) , "%s name '%s' is too short for the scope of its usage" , variable . kindName ( ) , variable . name )
2021-11-16 20:07:53 +00:00
}
2022-02-24 16:33:24 +00:00
}
2021-11-16 20:07:53 +00:00
2022-02-24 16:33:24 +00:00
// checkParams applies v to parameters in paramToDist.
func ( v * varNameLen ) checkParams ( pass * analysis . Pass , paramToDist map [ parameter ] int ) {
2021-11-16 20:07:53 +00:00
for param , dist := range paramToDist {
2022-02-24 16:33:24 +00:00
if v . ignoreNames . contains ( param . name ) {
2021-11-16 20:07:53 +00:00
continue
}
2022-02-24 16:33:24 +00:00
if v . ignoreDeclarations . matchParameter ( param ) {
continue
}
2021-11-16 20:07:53 +00:00
if v . checkNameAndDistance ( param . name , dist ) {
continue
}
2022-02-24 16:33:24 +00:00
if param . isConventional ( ) {
continue
}
2021-11-16 20:07:53 +00:00
pass . Reportf ( param . field . Pos ( ) , "parameter name '%s' is too short for the scope of its usage" , param . name )
}
2022-02-24 16:33:24 +00:00
}
2021-11-16 20:07:53 +00:00
2022-02-24 16:33:24 +00:00
// checkReturns applies v to named return values in returnToDist.
func ( v * varNameLen ) checkReturns ( pass * analysis . Pass , returnToDist map [ parameter ] int ) {
for returnValue , dist := range returnToDist {
if v . ignoreNames . contains ( returnValue . name ) {
2021-11-16 20:07:53 +00:00
continue
}
2022-02-24 16:33:24 +00:00
if v . ignoreDeclarations . matchParameter ( returnValue ) {
continue
}
if v . checkNameAndDistance ( returnValue . name , dist ) {
continue
}
pass . Reportf ( returnValue . field . Pos ( ) , "return value name '%s' is too short for the scope of its usage" , returnValue . name )
2021-11-16 20:07:53 +00:00
}
}
2022-02-24 16:33:24 +00:00
// checkNameAndDistance returns true if name or dist are considered "short".
2021-11-16 20:07:53 +00:00
func ( v * varNameLen ) checkNameAndDistance ( name string , dist int ) bool {
if len ( name ) >= v . minNameLength {
return true
}
2022-02-24 16:33:24 +00:00
2021-11-16 20:07:53 +00:00
if dist <= v . maxDistance {
return true
}
2022-02-24 16:33:24 +00:00
2021-11-16 20:07:53 +00:00
return false
}
2022-02-24 16:33:24 +00:00
// checkTypeAssertOk returns true if "ok" variables that hold the bool return value of a type assertion
// should be ignored, and if vari is such a variable.
func ( v * varNameLen ) checkTypeAssertOk ( vari variable ) bool {
return v . ignoreTypeAssertOk && vari . isTypeAssertOk ( )
}
// checkMapIndexOk returns true if "ok" variables that hold the bool return value of a map index
// should be ignored, and if vari is such a variable.
func ( v * varNameLen ) checkMapIndexOk ( vari variable ) bool {
return v . ignoreMapIndexOk && vari . isMapIndexOk ( )
}
// checkChannelReceiveOk returns true if "ok" variables that hold the bool return value of a channel receive
// should be ignored, and if vari is such a variable.
func ( v * varNameLen ) checkChannelReceiveOk ( vari variable ) bool {
return v . ignoreChannelReceiveOk && vari . isChannelReceiveOk ( )
}
// distances returns maps of variables, parameters, and return values mapping to their longest usage distances.
2021-11-16 20:07:53 +00:00
func ( v * varNameLen ) distances ( pass * analysis . Pass ) ( map [ variable ] int , map [ parameter ] int , map [ parameter ] int ) {
2022-02-24 16:33:24 +00:00
assignIdents , valueSpecIdents , paramIdents , returnIdents , imports := v . identsAndImports ( pass )
2021-11-16 20:07:53 +00:00
varToDist := map [ variable ] int { }
for _ , ident := range assignIdents {
2022-02-24 16:33:24 +00:00
assign := ident . Obj . Decl . ( * ast . AssignStmt ) //nolint:forcetypeassert // check is done in identsAndImports
2021-11-16 20:07:53 +00:00
variable := variable {
name : ident . Name ,
2022-02-24 16:33:24 +00:00
typ : shortTypeName ( pass . TypesInfo . TypeOf ( identAssignExpr ( ident , assign ) ) , imports ) ,
2021-11-16 20:07:53 +00:00
assign : assign ,
}
useLine := pass . Fset . Position ( ident . NamePos ) . Line
declLine := pass . Fset . Position ( assign . Pos ( ) ) . Line
varToDist [ variable ] = useLine - declLine
}
2022-02-24 16:33:24 +00:00
for _ , ident := range valueSpecIdents {
valueSpec := ident . Obj . Decl . ( * ast . ValueSpec ) //nolint:forcetypeassert // check is done in identsAndImports
variable := variable {
name : ident . Name ,
constant : ident . Obj . Kind == ast . Con ,
typ : shortTypeName ( pass . TypesInfo . TypeOf ( valueSpec . Type ) , imports ) ,
valueSpec : valueSpec ,
}
useLine := pass . Fset . Position ( ident . NamePos ) . Line
declLine := pass . Fset . Position ( valueSpec . Pos ( ) ) . Line
varToDist [ variable ] = useLine - declLine
}
2021-11-16 20:07:53 +00:00
paramToDist := map [ parameter ] int { }
for _ , ident := range paramIdents {
2022-02-24 16:33:24 +00:00
field := ident . Obj . Decl . ( * ast . Field ) //nolint:forcetypeassert // check is done in identsAndImports
2021-11-16 20:07:53 +00:00
param := parameter {
name : ident . Name ,
2022-02-24 16:33:24 +00:00
typ : shortTypeName ( pass . TypesInfo . TypeOf ( field . Type ) , imports ) ,
2021-11-16 20:07:53 +00:00
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 {
2022-02-24 16:33:24 +00:00
field := ident . Obj . Decl . ( * ast . Field ) //nolint:forcetypeassert // check is done in identsAndImports
2021-11-16 20:07:53 +00:00
param := parameter {
name : ident . Name ,
2022-02-24 16:33:24 +00:00
typ : shortTypeName ( pass . TypesInfo . TypeOf ( field . Type ) , imports ) ,
2021-11-16 20:07:53 +00:00
field : field ,
}
useLine := pass . Fset . Position ( ident . NamePos ) . Line
declLine := pass . Fset . Position ( field . Pos ( ) ) . Line
returnToDist [ param ] = useLine - declLine
}
return varToDist , paramToDist , returnToDist
}
2022-02-24 16:33:24 +00:00
// identsAndImports returns Idents referencing assign statements, value specifications, parameters, and return values, respectively,
// as well as import declarations.
func ( v * varNameLen ) identsAndImports ( pass * analysis . Pass ) ( [ ] * ast . Ident , [ ] * ast . Ident , [ ] * ast . Ident , [ ] * ast . Ident , [ ] importDeclaration ) { //nolint:gocognit,cyclop // this is complex stuff
inspector := pass . ResultOf [ inspect . Analyzer ] . ( * inspector . Inspector ) //nolint:forcetypeassert // inspect.Analyzer always returns *inspector.Inspector
2021-11-16 20:07:53 +00:00
filter := [ ] ast . Node {
2022-02-24 16:33:24 +00:00
( * ast . ImportSpec ) ( nil ) ,
2021-11-16 20:07:53 +00:00
( * ast . FuncDecl ) ( nil ) ,
2022-02-24 16:33:24 +00:00
( * ast . Ident ) ( nil ) ,
2021-11-16 20:07:53 +00:00
}
funcs := [ ] * ast . FuncDecl { }
methods := [ ] * ast . FuncDecl { }
2022-02-24 16:33:24 +00:00
imports := [ ] importDeclaration { }
2021-11-16 20:07:53 +00:00
assignIdents := [ ] * ast . Ident { }
2022-02-24 16:33:24 +00:00
valueSpecIdents := [ ] * ast . Ident { }
2021-11-16 20:07:53 +00:00
paramIdents := [ ] * ast . Ident { }
returnIdents := [ ] * ast . Ident { }
inspector . Preorder ( filter , func ( node ast . Node ) {
2022-02-24 16:33:24 +00:00
switch node2 := node . ( type ) {
case * ast . ImportSpec :
decl , ok := importSpecToDecl ( node2 , pass . Pkg . Imports ( ) )
if ! ok {
return
2021-11-16 20:07:53 +00:00
}
2022-02-24 16:33:24 +00:00
imports = append ( imports , decl )
2021-11-16 20:07:53 +00:00
2022-02-24 16:33:24 +00:00
case * ast . FuncDecl :
funcs = append ( funcs , node2 )
2021-11-16 20:07:53 +00:00
2022-02-24 16:33:24 +00:00
if node2 . Recv == nil {
2021-11-16 20:07:53 +00:00
return
}
2022-02-24 16:33:24 +00:00
methods = append ( methods , node2 )
case * ast . Ident :
if node2 . Obj == nil {
2021-11-16 20:07:53 +00:00
return
}
2022-02-24 16:33:24 +00:00
switch objDecl := node2 . Obj . Decl . ( type ) {
case * ast . AssignStmt :
assignIdents = append ( assignIdents , node2 )
case * ast . ValueSpec :
valueSpecIdents = append ( valueSpecIdents , node2 )
case * ast . Field :
if isReceiver ( objDecl , methods ) && ! v . checkReceiver {
return
}
if isReturn ( objDecl , funcs ) {
if ! v . checkReturn {
return
}
returnIdents = append ( returnIdents , node2 )
return
}
paramIdents = append ( paramIdents , node2 )
}
2021-11-16 20:07:53 +00:00
}
} )
2022-02-24 16:33:24 +00:00
imports = append ( imports , importDeclaration {
path : pass . Pkg . Path ( ) ,
self : true ,
} )
return assignIdents , valueSpecIdents , paramIdents , returnIdents , imports
2021-11-16 20:07:53 +00:00
}
2022-02-24 16:33:24 +00:00
func importSpecToDecl ( spec * ast . ImportSpec , imports [ ] * types . Package ) ( importDeclaration , bool ) {
path := strings . TrimSuffix ( strings . TrimPrefix ( spec . Path . Value , "\"" ) , "\"" )
if spec . Name != nil {
return importDeclaration {
name : spec . Name . Name ,
path : path ,
} , true
}
for _ , imp := range imports {
if imp . Path ( ) == path {
return importDeclaration {
name : imp . Name ( ) ,
path : path ,
} , true
}
}
return importDeclaration { } , false
}
// isTypeAssertOk returns true if v is an "ok" variable that holds the bool return value of a type assertion.
func ( v variable ) isTypeAssertOk ( ) bool {
if v . name != "ok" {
return false
}
if v . assign == nil {
return false
}
if len ( v . assign . Lhs ) != 2 {
return false
}
ident , ok := v . assign . Lhs [ 1 ] . ( * ast . Ident )
if ! ok {
return false
}
if ident . Name != "ok" {
return false
}
if len ( v . assign . Rhs ) != 1 {
return false
}
if _ , ok := v . assign . Rhs [ 0 ] . ( * ast . TypeAssertExpr ) ; ! ok {
return false
}
return true
}
// isMapIndexOk returns true if v is an "ok" variable that holds the bool return value of a map index.
func ( v variable ) isMapIndexOk ( ) bool {
if v . name != "ok" {
return false
}
if v . assign == nil {
return false
}
if len ( v . assign . Lhs ) != 2 {
return false
}
ident , ok := v . assign . Lhs [ 1 ] . ( * ast . Ident )
if ! ok {
return false
}
if ident . Name != "ok" {
return false
}
if len ( v . assign . Rhs ) != 1 {
return false
}
if _ , ok := v . assign . Rhs [ 0 ] . ( * ast . IndexExpr ) ; ! ok {
return false
}
return true
}
// isChannelReceiveOk returns true if v is an "ok" variable that holds the bool return value of a channel receive.
func ( v variable ) isChannelReceiveOk ( ) bool {
if v . name != "ok" {
return false
}
if v . assign == nil {
return false
}
if len ( v . assign . Lhs ) != 2 {
return false
}
ident , ok := v . assign . Lhs [ 1 ] . ( * ast . Ident )
if ! ok {
return false
}
if ident . Name != "ok" {
return false
}
if len ( v . assign . Rhs ) != 1 {
return false
}
unary , ok := v . assign . Rhs [ 0 ] . ( * ast . UnaryExpr )
if ! ok {
return false
}
if unary . Op != token . ARROW {
return false
}
return true
}
// match returns true if v matches decl.
func ( v variable ) match ( decl declaration ) bool {
if v . name != decl . name {
return false
}
if v . constant != decl . constant {
return false
}
if v . constant {
return true
}
if v . typ == "" {
return false
}
return decl . matchType ( v . typ )
}
// kindName returns "constant" if v.constant==true, else "variable".
func ( v variable ) kindName ( ) string {
if v . constant {
return "constant"
}
return "variable"
}
// isReceiver returns true if field is a receiver parameter of any of the given methods.
2021-11-16 20:07:53 +00:00
func isReceiver ( field * ast . Field , methods [ ] * ast . FuncDecl ) bool {
for _ , m := range methods {
for _ , recv := range m . Recv . List {
if recv == field {
return true
}
}
}
2022-02-24 16:33:24 +00:00
2021-11-16 20:07:53 +00:00
return false
}
2022-02-24 16:33:24 +00:00
// isReturn returns true if field is a return value of any of the given funcs.
2021-11-16 20:07:53 +00:00
func isReturn ( field * ast . Field , funcs [ ] * ast . FuncDecl ) bool {
for _ , f := range funcs {
if f . Type . Results == nil {
continue
}
2022-02-24 16:33:24 +00:00
2021-11-16 20:07:53 +00:00
for _ , r := range f . Type . Results . List {
if r == field {
return true
}
}
}
2022-02-24 16:33:24 +00:00
return false
2021-11-16 20:07:53 +00:00
}
2022-02-24 16:33:24 +00:00
// isConventional returns true if p is a conventional Go parameter, such as "ctx context.Context" or
// "t *testing.T".
func ( p parameter ) isConventional ( ) bool {
for _ , decl := range conventionalDecls {
if p . match ( decl ) {
2021-11-16 20:07:53 +00:00
return true
}
}
2022-02-24 16:33:24 +00:00
2021-11-16 20:07:53 +00:00
return false
}
2022-02-24 16:33:24 +00:00
// match returns whether p matches decl.
func ( p parameter ) match ( decl declaration ) bool {
if p . name != decl . name {
2021-11-16 20:07:53 +00:00
return false
}
2022-02-24 16:33:24 +00:00
return decl . matchType ( p . typ )
2021-11-16 20:07:53 +00:00
}
2022-02-24 16:33:24 +00:00
// parseDeclaration parses and returns a variable declaration parsed from decl.
func parseDeclaration ( decl string ) declaration {
if strings . HasPrefix ( decl , "const " ) {
return declaration {
name : strings . TrimPrefix ( decl , "const " ) ,
constant : true ,
}
2021-11-16 20:07:53 +00:00
}
2022-02-24 16:33:24 +00:00
parts := strings . SplitN ( decl , " " , 2 )
return declaration {
name : parts [ 0 ] ,
typ : parts [ 1 ] ,
2021-11-16 20:07:53 +00:00
}
2022-02-24 16:33:24 +00:00
}
// matchType returns true if typ matches d.typ.
func ( d declaration ) matchType ( typ string ) bool {
return d . typ == typ
}
// identAssignExpr returns the expression that is assigned to ident.
//
// TODO: This currently only works for simple one-to-one assignments without the use of multi-values.
func identAssignExpr ( _ * ast . Ident , assign * ast . AssignStmt ) ast . Expr {
if len ( assign . Lhs ) != 1 || len ( assign . Rhs ) != 1 {
return nil
2021-11-16 20:07:53 +00:00
}
2022-02-24 16:33:24 +00:00
return assign . Rhs [ 0 ]
2021-11-16 20:07:53 +00:00
}
2022-02-24 16:33:24 +00:00
// shortTypeName returns the short name of typ, with respect to imports.
// For example, if package github.com/matryer/is is imported with alias "x",
// and typ represents []*github.com/matryer/is.I, shortTypeName will return "[]*x.I".
// For imports without aliases, the package's default name will be used.
func shortTypeName ( typ types . Type , imports [ ] importDeclaration ) string {
if typ == nil {
return ""
2021-11-16 20:07:53 +00:00
}
2022-02-24 16:33:24 +00:00
typStr := typ . String ( )
for _ , imp := range imports {
prefix := imp . path + "."
if imp . self {
typStr = strings . ReplaceAll ( typStr , prefix , "" )
continue
}
typStr = strings . ReplaceAll ( typStr , prefix , imp . name + "." )
}
return typStr
2021-11-16 20:07:53 +00:00
}