Skip to content

Commit

Permalink
Performance improvements (#5)
Browse files Browse the repository at this point in the history
* feat: add type check decision cache

As regexps are heavy, it is a good idea to cache check result and
preform checks only once per type.

* feat: add struct fields cache

Also improve code splitting.
  • Loading branch information
xobotyi authored Jun 10, 2022
1 parent 5e5cd4e commit 2a7dfaf
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 107 deletions.
6 changes: 5 additions & 1 deletion .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
197 changes: 91 additions & 106 deletions pkg/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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

Expand All @@ -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,
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
}

Expand All @@ -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)
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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
}
68 changes: 68 additions & 0 deletions pkg/analyzer/patterns-list.go
Original file line number Diff line number Diff line change
@@ -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
}
27 changes: 27 additions & 0 deletions pkg/analyzer/struct-fields.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 2a7dfaf

Please sign in to comment.