diff --git a/.golangci.yaml b/.golangci.yaml index 81502fb7..f9e363e4 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -6,9 +6,13 @@ linters: disable: - varnamelen - nonamedreturns - - exhaustivestruct - gofumpt - gci + - exhaustivestruct # deprecated + - scopelint # deprecated + - golint # deprecated + - interfacer # deprecated + - maligned # deprecated issues: max-issues-per-linter: 0 diff --git a/pkg/analyzer/analyzer.go b/pkg/analyzer/analyzer.go index 6719ac5c..49948e12 100644 --- a/pkg/analyzer/analyzer.go +++ b/pkg/analyzer/analyzer.go @@ -3,11 +3,10 @@ package analyzer import ( "errors" "flag" - "fmt" "go/ast" "go/types" - "regexp" "strings" + "sync" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" @@ -21,13 +20,23 @@ var ( type analyzer struct { include PatternsList exclude PatternsList + + typesProcessCache map[string]bool + typesProcessCacheMu sync.RWMutex + + structFieldsCache map[string]*StructFields + structFieldsCacheMu sync.RWMutex } // NewAnalyzer returns a go/analysis-compatible analyzer. // -i arguments adds include patterns // -e arguments adds exclude patterns func NewAnalyzer(include []string, exclude []string) (*analysis.Analyzer, error) { - a := analyzer{} + a := analyzer{ //nolint:exhaustruct + typesProcessCache: map[string]bool{}, + + structFieldsCache: map[string]*StructFields{}, + } var err error @@ -41,7 +50,7 @@ func NewAnalyzer(include []string, exclude []string) (*analysis.Analyzer, error) return nil, err } - return &analysis.Analyzer{ + return &analysis.Analyzer{ //nolint:exhaustruct Name: "exhaustruct", Doc: "Checks if all structure fields are initialized", Run: a.run, @@ -80,7 +89,7 @@ func (a *analyzer) run(pass *analysis.Pass) (interface{}, error) { return nil, nil //nolint:nilnil } -//nolint:funlen,cyclop +//nolint:cyclop func (a *analyzer) newVisitor(pass *analysis.Pass) func(node ast.Node) { var ret *ast.ReturnStmt @@ -114,13 +123,7 @@ func (a *analyzer) newVisitor(pass *analysis.Pass) func(node ast.Node) { return } - if len(a.include) > 0 { - if !a.include.MatchesAny(typ.String()) { - return - } - } - - if a.exclude.MatchesAny(typ.String()) { + if !a.shouldProcessType(typ.String()) { return } @@ -135,7 +138,7 @@ func (a *analyzer) newVisitor(pass *analysis.Pass) func(node ast.Node) { } } - missingFields := structMissingFields(lit, strct, typ, pass) + missingFields := a.structMissingFields(lit, strct, typ.String(), pass.Pkg.Path()) if len(missingFields) == 1 { pass.Reportf(node.Pos(), "%s is missing in %s", missingFields[0], strctName) @@ -145,6 +148,77 @@ func (a *analyzer) newVisitor(pass *analysis.Pass) func(node ast.Node) { } } +func (a *analyzer) shouldProcessType(typ string) bool { + if len(a.include) == 0 && len(a.exclude) == 0 { + // skip whole part with cache, since we have no restrictions and have to check everything + return true + } + + a.typesProcessCacheMu.RLock() + v, ok := a.typesProcessCache[typ] + a.typesProcessCacheMu.RUnlock() + + if !ok { + a.typesProcessCacheMu.Lock() + defer a.typesProcessCacheMu.Unlock() + + v = true + + if len(a.include) > 0 && !a.include.MatchesAny(typ) { + v = false + } + + if v && a.exclude.MatchesAny(typ) { + v = false + } + + a.typesProcessCache[typ] = v + } + + return v +} + +func (a *analyzer) structMissingFields( + lit *ast.CompositeLit, + strct *types.Struct, + typ string, + pkgPath string, +) []string { + keys, unnamed := literalKeys(lit) + fields := a.structFields(typ, strct) + + var fieldNames []string + + if strings.HasPrefix(typ, pkgPath+".") { + // we're in same package and should match private fields + fieldNames = fields.All + } else { + fieldNames = fields.Public + } + + if unnamed { + return fieldNames[len(keys):] + } + + return difference(fieldNames, keys) +} + +func (a *analyzer) structFields(typ string, strct *types.Struct) *StructFields { + a.structFieldsCacheMu.RLock() + fields, ok := a.structFieldsCache[typ] + a.structFieldsCacheMu.RUnlock() + + if !ok { + a.structFieldsCacheMu.Lock() + defer a.structFieldsCacheMu.Unlock() + + fields = NewStructFields(strct) + a.structFieldsCache[typ] = fields + } + + return fields +} + func returnContainsLiteral(ret *ast.ReturnStmt, lit *ast.CompositeLit) bool { for _, result := range ret.Results { if l, ok := result.(*ast.CompositeLit); ok { @@ -167,19 +241,6 @@ func returnContainsError(ret *ast.ReturnStmt, pass *analysis.Pass) bool { return false } -func structMissingFields(lit *ast.CompositeLit, strct *types.Struct, typ types.Type, pass *analysis.Pass) []string { - isSamePackage := strings.HasPrefix(typ.String(), pass.Pkg.Path()+".") - - keys, unnamed := literalKeys(lit) - fields := structFields(strct, isSamePackage) - - if unnamed { - return fields[len(keys):] - } - - return difference(fields, keys) -} - func literalKeys(lit *ast.CompositeLit) (keys []string, unnamed bool) { for _, elt := range lit.Elts { if k, ok := elt.(*ast.KeyValueExpr); ok { @@ -195,31 +256,17 @@ func literalKeys(lit *ast.CompositeLit) (keys []string, unnamed bool) { unnamed = true keys = make([]string, len(lit.Elts)) - break - } - - return keys, unnamed -} - -func structFields(strct *types.Struct, withPrivate bool) (keys []string) { - for i := 0; i < strct.NumFields(); i++ { - f := strct.Field(i) - - if !f.Exported() && !withPrivate { - continue - } - - keys = append(keys, f.Name()) + return } - return keys + return } // difference returns elements that are in `a` and not in `b`. func difference(a, b []string) (diff []string) { - mb := make(map[string]bool, len(b)) + mb := make(map[string]struct{}, len(b)) for _, x := range b { - mb[x] = true + mb[x] = struct{}{} } for _, x := range a { @@ -243,65 +290,3 @@ func exprName(expr ast.Expr) string { return s.Sel.Name } - -type PatternsList []*regexp.Regexp - -// MatchesAny matches provided string against all regexps in a slice. -func (l PatternsList) MatchesAny(str string) bool { - for _, r := range l { - if r.MatchString(str) { - return true - } - } - - return false -} - -// newPatternsList parses slice of strings to a slice of compiled regular -// expressions. -func newPatternsList(in []string) (PatternsList, error) { - list := PatternsList{} - - for _, str := range in { - re, err := strToRegexp(str) - if err != nil { - return nil, err - } - - list = append(list, re) - } - - return list, nil -} - -type reListVar struct { - values *PatternsList -} - -func (v *reListVar) Set(value string) error { - re, err := strToRegexp(value) - if err != nil { - return err - } - - *v.values = append(*v.values, re) - - return nil -} - -func (v *reListVar) String() string { - return "" -} - -func strToRegexp(str string) (*regexp.Regexp, error) { - if str == "" { - return nil, ErrEmptyPattern - } - - re, err := regexp.Compile(str) - if err != nil { - return nil, fmt.Errorf("unable to compile %s as regular expression: %w", str, err) - } - - return re, nil -} diff --git a/pkg/analyzer/patterns-list.go b/pkg/analyzer/patterns-list.go new file mode 100644 index 00000000..2884cab6 --- /dev/null +++ b/pkg/analyzer/patterns-list.go @@ -0,0 +1,68 @@ +package analyzer + +import ( + "fmt" + "regexp" +) + +type PatternsList []*regexp.Regexp + +// MatchesAny matches provided string against all regexps in a slice. +func (l PatternsList) MatchesAny(str string) bool { + for _, r := range l { + if r.MatchString(str) { + return true + } + } + + return false +} + +// newPatternsList parses slice of strings to a slice of compiled regular +// expressions. +func newPatternsList(in []string) (PatternsList, error) { + list := PatternsList{} + + for _, str := range in { + re, err := strToRegexp(str) + if err != nil { + return nil, err + } + + list = append(list, re) + } + + return list, nil +} + +type reListVar struct { + values *PatternsList +} + +func (v *reListVar) Set(value string) error { + re, err := strToRegexp(value) + if err != nil { + return err + } + + *v.values = append(*v.values, re) + + return nil +} + +func (v *reListVar) String() string { + return "" +} + +func strToRegexp(str string) (*regexp.Regexp, error) { + if str == "" { + return nil, ErrEmptyPattern + } + + re, err := regexp.Compile(str) + if err != nil { + return nil, fmt.Errorf("unable to compile %s as regular expression: %w", str, err) + } + + return re, nil +} diff --git a/pkg/analyzer/struct-fields.go b/pkg/analyzer/struct-fields.go new file mode 100644 index 00000000..e7d00425 --- /dev/null +++ b/pkg/analyzer/struct-fields.go @@ -0,0 +1,27 @@ +package analyzer + +import ( + "go/types" +) + +type StructFields struct { + Public []string + + All []string +} + +func NewStructFields(strct *types.Struct) *StructFields { + sf := StructFields{} //nolint:exhaustruct + + for i := 0; i < strct.NumFields(); i++ { + f := strct.Field(i) + + sf.All = append(sf.All, f.Name()) + + if f.Exported() { + sf.Public = append(sf.Public, f.Name()) + } + } + + return &sf +}