Skip to content

Commit

Permalink
feat: add struct fields cache
Browse files Browse the repository at this point in the history
Also improve code splitting.
  • Loading branch information
xobotyi committed Jun 10, 2022
1 parent a0fba79 commit 5b6eeb2
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 99 deletions.
154 changes: 55 additions & 99 deletions pkg/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ package analyzer
import (
"errors"
"flag"
"fmt"
"go/ast"
"go/types"
"regexp"
"strings"
"sync"

Expand All @@ -25,6 +23,9 @@ type analyzer struct {

typesProcessCache map[string]bool
typesProcessCacheMu sync.RWMutex

structFieldsCache map[string]*StructFields
structFieldsCacheMu sync.RWMutex
}

// NewAnalyzer returns a go/analysis-compatible analyzer.
Expand All @@ -33,6 +34,8 @@ type analyzer struct {
func NewAnalyzer(include []string, exclude []string) (*analysis.Analyzer, error) {
a := analyzer{ //nolint:exhaustruct
typesProcessCache: map[string]bool{},

structFieldsCache: map[string]*StructFields{},
}

var err error
Expand Down Expand Up @@ -120,7 +123,7 @@ func (a *analyzer) newVisitor(pass *analysis.Pass) func(node ast.Node) {
return
}

if !a.shouldProcess(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,7 +148,7 @@ func (a *analyzer) newVisitor(pass *analysis.Pass) func(node ast.Node) {
}
}

func (a *analyzer) shouldProcess(typ string) bool {
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
Expand All @@ -157,6 +160,8 @@ func (a *analyzer) shouldProcess(typ string) bool {

if !ok {
a.typesProcessCacheMu.Lock()
defer a.typesProcessCacheMu.Unlock()

v = true

if len(a.include) > 0 && !a.include.MatchesAny(typ) {
Expand All @@ -168,12 +173,52 @@ func (a *analyzer) shouldProcess(typ string) bool {
}

a.typesProcessCache[typ] = v
a.typesProcessCacheMu.Unlock()
}

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 @@ -196,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 @@ -224,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 @@ -272,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 5b6eeb2

Please sign in to comment.