Skip to content

Commit

Permalink
[feat] allow matching and exclusion of anonymous structs (#65)
Browse files Browse the repository at this point in the history
  • Loading branch information
xobotyi committed Jul 17, 2023
1 parent 1841a4a commit c7dc19f
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 35 deletions.
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,18 @@ exhaustruct [-flag] [package]
Flags:
-i value
Regular expression to match structures, can receive multiple flags
Regular expression to match type names, can receive multiple flags.
Anonymous structs can be matched by '<anonymous>' alias.
4ex:
github.com/GaijinEntertainment/go-exhaustruct/v3/analyzer\.<anonymous>
github.com/GaijinEntertainment/go-exhaustruct/v3/analyzer\.TypeInfo
-e value
Regular expression to exclude structures, can receive multiple flags
Regular expression to exclude type names, can receive multiple flags.
Anonymous structs can be matched by '<anonymous>' alias.
4ex:
github.com/GaijinEntertainment/go-exhaustruct/v3/analyzer\.<anonymous>
github.com/GaijinEntertainment/go-exhaustruct/v3/analyzer\.TypeInfo
```

### Example
Expand Down
86 changes: 58 additions & 28 deletions analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ type analyzer struct {
fieldsCache map[types.Type]fields.StructFields
fieldsCacheMu sync.RWMutex `exhaustruct:"optional"`

typeProcessingNeed map[types.Type]bool
typeProcessingNeed map[string]bool
typeProcessingNeedMu sync.RWMutex `exhaustruct:"optional"`
}

func NewAnalyzer(include, exclude []string) (*analysis.Analyzer, error) {
a := analyzer{
fieldsCache: make(map[types.Type]fields.StructFields),
typeProcessingNeed: make(map[types.Type]bool),
typeProcessingNeed: make(map[string]bool),
}

var err error
Expand All @@ -57,8 +57,16 @@ func NewAnalyzer(include, exclude []string) (*analysis.Analyzer, error) {
func (a *analyzer) newFlagSet() flag.FlagSet {
fs := flag.NewFlagSet("", flag.PanicOnError)

fs.Var(&a.include, "i", "Regular expression to match structures, can receive multiple flags")
fs.Var(&a.exclude, "e", "Regular expression to exclude structures, can receive multiple flags")
fs.Var(&a.include, "i", `Regular expression to match type names, can receive multiple flags.
Anonymous structs can be matched by '<anonymous>' alias.
4ex:
github.com/GaijinEntertainment/go-exhaustruct/v3/analyzer\.<anonymous>
github.com/GaijinEntertainment/go-exhaustruct/v3/analyzer\.TypeInfo`)
fs.Var(&a.exclude, "e", `Regular expression to exclude type names, can receive multiple flags.
Anonymous structs can be matched by '<anonymous>' alias.
4ex:
github.com/GaijinEntertainment/go-exhaustruct/v3/analyzer\.<anonymous>
github.com/GaijinEntertainment/go-exhaustruct/v3/analyzer\.TypeInfo`)

return *fs
}
Expand Down Expand Up @@ -89,7 +97,7 @@ func (a *analyzer) newVisitor(pass *analysis.Pass) func(n ast.Node, push bool, s
return true
}

structTyp, namedTyp, ok := getStructType(pass, lit)
structTyp, typeInfo, ok := getStructType(pass, lit)
if !ok {
return true
}
Expand All @@ -100,14 +108,14 @@ func (a *analyzer) newVisitor(pass *analysis.Pass) func(n ast.Node, push bool, s
// it is okay to return uninitialized structure in case struct's direct parent is
// a return statement containing non-nil error
//
// we're unable to check if returned error is custom, but at leas we're able to
// we're unable to check if returned error is custom, but at least we're able to
// cover str [error] type.
return true
}
}
}

pos, msg := a.processStruct(pass, lit, structTyp, namedTyp)
pos, msg := a.processStruct(pass, lit, structTyp, typeInfo)
if pos != nil {
pass.Reportf(*pos, msg)
}
Expand All @@ -116,17 +124,30 @@ func (a *analyzer) newVisitor(pass *analysis.Pass) func(n ast.Node, push bool, s
}
}

func getStructType(pass *analysis.Pass, lit *ast.CompositeLit) (*types.Struct, *types.Named, bool) {
func getStructType(pass *analysis.Pass, lit *ast.CompositeLit) (*types.Struct, *TypeInfo, bool) {
switch typ := pass.TypesInfo.TypeOf(lit).(type) {
case *types.Named: // named type
if structTyp, ok := typ.Underlying().(*types.Struct); ok {
return structTyp, typ, true
pkg := typ.Obj().Pkg()
ti := TypeInfo{
Name: typ.Obj().Name(),
PackageName: pkg.Name(),
PackagePath: pkg.Path(),
}

return structTyp, &ti, true
}

return nil, nil, false

case *types.Struct: // anonymous struct
return typ, nil, true
ti := TypeInfo{
Name: "<anonymous>",
PackageName: pass.Pkg.Name(),
PackagePath: pass.Pkg.Path(),
}

return typ, &ti, true

default:
return nil, nil, false
Expand Down Expand Up @@ -157,60 +178,55 @@ func (a *analyzer) processStruct(
pass *analysis.Pass,
lit *ast.CompositeLit,
structTyp *types.Struct,
namedTyp *types.Named,
info *TypeInfo,
) (*token.Pos, string) {
if !a.shouldProcessType(namedTyp) {
if !a.shouldProcessType(info) {
return nil, ""
}

// unnamed structures are only defined in same package, along with types that has
// prefix identical to current package name.
isSamePackage := namedTyp == nil || pass.Pkg.Scope().Lookup(namedTyp.Obj().Name()) != nil
isSamePackage := info.PackagePath == pass.Pkg.Path()

if f := a.litSkippedFields(lit, structTyp, !isSamePackage); len(f) > 0 {
structName := "anonymous struct"
if namedTyp != nil {
structName = namedTyp.Obj().Pkg().Name() + "." + namedTyp.Obj().Name()
}

pos := lit.Pos()

if len(f) == 1 {
return &pos, fmt.Sprintf("%s is missing field %s", structName, f.String())
return &pos, fmt.Sprintf("%s is missing field %s", info.ShortString(), f.String())
}

return &pos, fmt.Sprintf("%s is missing fields %s", structName, f.String())
return &pos, fmt.Sprintf("%s is missing fields %s", info.ShortString(), f.String())
}

return nil, ""
}

// shouldProcessType returns true if type should be processed basing off include
// and exclude patterns, defined though constructor and\or flags.
func (a *analyzer) shouldProcessType(typ *types.Named) bool {
if typ == nil || (len(a.include) == 0 && len(a.exclude) == 0) {
// anonymous structs or in case no filtering configured
func (a *analyzer) shouldProcessType(info *TypeInfo) bool {
if len(a.include) == 0 && len(a.exclude) == 0 {
return true
}

name := info.String()

a.typeProcessingNeedMu.RLock()
res, ok := a.typeProcessingNeed[typ]
res, ok := a.typeProcessingNeed[name]
a.typeProcessingNeedMu.RUnlock()

if !ok {
a.typeProcessingNeedMu.Lock()
typStr := typ.String()
res = true

if a.include != nil && !a.include.MatchFullString(typStr) {
if a.include != nil && !a.include.MatchFullString(name) {
res = false
}

if res && a.exclude != nil && a.exclude.MatchFullString(typStr) {
if res && a.exclude != nil && a.exclude.MatchFullString(name) {
res = false
}

a.typeProcessingNeed[typ] = res
a.typeProcessingNeed[name] = res
a.typeProcessingNeedMu.Unlock()
}

Expand All @@ -236,3 +252,17 @@ func (a *analyzer) litSkippedFields(

return f.SkippedFields(lit, onlyExported)
}

type TypeInfo struct {
Name string
PackageName string
PackagePath string
}

func (t TypeInfo) String() string {
return t.PackagePath + "." + t.Name
}

func (t TypeInfo) ShortString() string {
return t.PackageName + "." + t.Name
}
4 changes: 2 additions & 2 deletions analyzer/analyzer_benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import (

func BenchmarkAnalyzer(b *testing.B) {
a, err := analyzer.NewAnalyzer(
[]string{`.*[Tt]est.*`, `.*External`, `.*Embedded`},
[]string{`.*Excluded$`},
[]string{`.*[Tt]est.*`, `.*External`, `.*Embedded`, `.*\.<anonymous>`},
[]string{`.*Excluded$`, `e\.<anonymous>`},
)
require.NoError(b, err)

Expand Down
6 changes: 3 additions & 3 deletions analyzer/analyzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ func TestAnalyzer(t *testing.T) {
assert.Error(t, err)

a, err = analyzer.NewAnalyzer(
[]string{`.*[Tt]est.*`, `.*External`, `.*Embedded`},
[]string{`.*Excluded$`},
[]string{`.*[Tt]est.*`, `.*External`, `.*Embedded`, `.*\.<anonymous>`},
[]string{`.*Excluded$`, `e\.<anonymous>`},
)
require.NoError(t, err)

analysistest.Run(t, testdataPath, a, "i")
analysistest.Run(t, testdataPath, a, "i", "e")
}
7 changes: 7 additions & 0 deletions analyzer/testdata/src/e/e.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,10 @@ type ExternalExcluded struct {
B string
c string
}

func shouldPassAnonymousExcludedStruct() {
_ = struct {
A string
B int
}{}
}
19 changes: 19 additions & 0 deletions analyzer/testdata/src/i/i.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,22 @@ func shouldFailMapOfStructs() {
func shouldPassSlice() {
_ = []string{"a", "b"}
}

func shouldPassAnonymousStruct() {
_ = struct {
A string
B int
}{
A: "a",
B: 1,
}
}

func shouldFailAnonymousStructUnfilled() {
_ = struct { // want "i.<anonymous> is missing field A"
A string
B int
}{
B: 1,
}
}

0 comments on commit c7dc19f

Please sign in to comment.