Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] allow matching and exclusion of anonymous structs #65

Merged
merged 1 commit into from
Jul 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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,
}
}