diff --git a/analyzer/analyzer.go b/analyzer/analyzer.go index 35ce1350..b490f1c6 100644 --- a/analyzer/analyzer.go +++ b/analyzer/analyzer.go @@ -13,18 +13,16 @@ import ( "golang.org/x/tools/go/ast/inspector" "github.com/GaijinEntertainment/go-exhaustruct/v3/internal/comment" - "github.com/GaijinEntertainment/go-exhaustruct/v3/internal/fields" "github.com/GaijinEntertainment/go-exhaustruct/v3/internal/pattern" + "github.com/GaijinEntertainment/go-exhaustruct/v3/internal/structure" ) type analyzer struct { include pattern.List `exhaustruct:"optional"` exclude pattern.List `exhaustruct:"optional"` - fieldsCache map[types.Type]fields.StructFields - fieldsCacheMu sync.RWMutex `exhaustruct:"optional"` - - comments comment.Cache + structFields structure.FieldsCache `exhaustruct:"optional"` + comments comment.Cache `exhaustruct:"optional"` typeProcessingNeed map[string]bool typeProcessingNeedMu sync.RWMutex `exhaustruct:"optional"` @@ -32,7 +30,6 @@ type analyzer struct { func NewAnalyzer(include, exclude []string) (*analysis.Analyzer, error) { a := analyzer{ - fieldsCache: make(map[types.Type]fields.StructFields), typeProcessingNeed: make(map[string]bool), comments: comment.Cache{}, } @@ -271,24 +268,12 @@ func (a *analyzer) shouldProcessType(info *TypeInfo) bool { return res } -//revive:disable-next-line:unused-receiver func (a *analyzer) litSkippedFields( lit *ast.CompositeLit, typ *types.Struct, onlyExported bool, -) fields.StructFields { - a.fieldsCacheMu.RLock() - f, ok := a.fieldsCache[typ] - a.fieldsCacheMu.RUnlock() - - if !ok { - a.fieldsCacheMu.Lock() - f = fields.NewStructFields(typ) - a.fieldsCache[typ] = f - a.fieldsCacheMu.Unlock() - } - - return f.SkippedFields(lit, onlyExported) +) structure.Fields { + return a.structFields.Get(typ).Skipped(lit, onlyExported) } type TypeInfo struct { diff --git a/internal/structure/fields-cache.go b/internal/structure/fields-cache.go new file mode 100644 index 00000000..12a37969 --- /dev/null +++ b/internal/structure/fields-cache.go @@ -0,0 +1,35 @@ +package structure + +import ( + "go/types" + "sync" +) + +type FieldsCache struct { + fields map[*types.Struct]Fields + mu sync.RWMutex +} + +// Get returns a struct fields for a given type. In case if a struct fields is +// not found, it creates a new one from type definition. +func (c *FieldsCache) Get(typ *types.Struct) Fields { + c.mu.RLock() + fields, ok := c.fields[typ] + c.mu.RUnlock() + + if ok { + return fields + } + + c.mu.Lock() + defer c.mu.Unlock() + + if c.fields == nil { + c.fields = make(map[*types.Struct]Fields) + } + + fields = NewFields(typ) + c.fields[typ] = fields + + return fields +} diff --git a/internal/fields/struct.go b/internal/structure/fields.go similarity index 64% rename from internal/fields/struct.go rename to internal/structure/fields.go index af2390e8..b6b1a48c 100644 --- a/internal/fields/struct.go +++ b/internal/structure/fields.go @@ -1,33 +1,34 @@ -package fields +package structure import ( "go/ast" "go/types" "reflect" + "strings" ) const ( - TagName = "exhaustruct" - OptionalTagValue = "optional" + tagName = "exhaustruct" + optionalTagValue = "optional" ) -type StructField struct { +type Field struct { Name string Exported bool Optional bool } -type StructFields []*StructField +type Fields []*Field -// NewStructFields creates a new [StructFields] from a given struct type. -// StructFields items are listed in order they appear in the struct. -func NewStructFields(strct *types.Struct) StructFields { - sf := make(StructFields, 0, strct.NumFields()) +// NewFields creates a new [Fields] from a given struct type. +// Fields items are listed in order they appear in the struct. +func NewFields(strct *types.Struct) Fields { + sf := make(Fields, 0, strct.NumFields()) for i := 0; i < strct.NumFields(); i++ { f := strct.Field(i) - sf = append(sf, &StructField{ + sf = append(sf, &Field{ Name: f.Name(), Exported: f.Exported(), Optional: HasOptionalTag(strct.Tag(i)), @@ -38,27 +39,29 @@ func NewStructFields(strct *types.Struct) StructFields { } func HasOptionalTag(tags string) bool { - return reflect.StructTag(tags).Get(TagName) == OptionalTagValue + return reflect.StructTag(tags).Get(tagName) == optionalTagValue } // String returns a comma-separated list of field names. -func (sf StructFields) String() (res string) { +func (sf Fields) String() string { + b := strings.Builder{} + for i := 0; i < len(sf); i++ { - if res != "" { - res += ", " + if b.Len() != 0 { + b.WriteString(", ") } - res += sf[i].Name + b.WriteString(sf[i].Name) } - return res + return b.String() } -// SkippedFields returns a list of fields that are not present in the given +// Skipped returns a list of fields that are not present in the given // literal, but expected to. // //revive:disable-next-line:cyclomatic -func (sf StructFields) SkippedFields(lit *ast.CompositeLit, onlyExported bool) StructFields { +func (sf Fields) Skipped(lit *ast.CompositeLit, onlyExported bool) Fields { if len(lit.Elts) != 0 && !isNamedLiteral(lit) { if len(lit.Elts) == len(sf) { return nil @@ -68,7 +71,7 @@ func (sf StructFields) SkippedFields(lit *ast.CompositeLit, onlyExported bool) S } em := sf.existenceMap() - res := make(StructFields, 0, len(sf)) + res := make(Fields, 0, len(sf)) for i := 0; i < len(lit.Elts); i++ { kv, ok := lit.Elts[i].(*ast.KeyValueExpr) @@ -99,7 +102,7 @@ func (sf StructFields) SkippedFields(lit *ast.CompositeLit, onlyExported bool) S return res } -func (sf StructFields) existenceMap() map[string]bool { +func (sf Fields) existenceMap() map[string]bool { m := make(map[string]bool, len(sf)) for i := 0; i < len(sf); i++ { diff --git a/internal/fields/struct_test.go b/internal/structure/fields_test.go similarity index 77% rename from internal/fields/struct_test.go rename to internal/structure/fields_test.go index bd9be620..406d694c 100644 --- a/internal/fields/struct_test.go +++ b/internal/structure/fields_test.go @@ -1,4 +1,4 @@ -package fields_test +package structure_test import ( "go/ast" @@ -9,14 +9,14 @@ import ( "github.com/stretchr/testify/suite" "golang.org/x/tools/go/packages" - "github.com/GaijinEntertainment/go-exhaustruct/v3/internal/fields" + "github.com/GaijinEntertainment/go-exhaustruct/v3/internal/structure" ) func Test_HasOptionalTag(t *testing.T) { t.Parallel() - assert.True(t, fields.HasOptionalTag(`exhaustruct:"optional"`)) - assert.False(t, fields.HasOptionalTag(`exhaustruct:"required"`)) + assert.True(t, structure.HasOptionalTag(`exhaustruct:"optional"`)) + assert.False(t, structure.HasOptionalTag(`exhaustruct:"required"`)) } func TestStructFields(t *testing.T) { @@ -47,7 +47,7 @@ func (s *StructFieldsSuite) SetupSuite() { s.Require().NotNil(s.scope) } -func (s *StructFieldsSuite) getReferenceStructFields() fields.StructFields { +func (s *StructFieldsSuite) getReferenceStructFields() structure.Fields { s.T().Helper() obj := s.scope.Lookup("testStruct") @@ -56,14 +56,14 @@ func (s *StructFieldsSuite) getReferenceStructFields() fields.StructFields { typ := s.pkg.TypesInfo.TypeOf(obj.Decl.(*ast.TypeSpec).Type) //nolint:forcetypeassert s.Require().NotNil(typ) - return fields.NewStructFields(typ.Underlying().(*types.Struct)) //nolint:forcetypeassert + return structure.NewFields(typ.Underlying().(*types.Struct)) //nolint:forcetypeassert } func (s *StructFieldsSuite) TestNewStructFields() { sf := s.getReferenceStructFields() s.Assert().Len(sf, 4) - s.Assert().Equal(fields.StructFields{ + s.Assert().Equal(structure.Fields{ { Name: "ExportedRequired", Exported: true, @@ -103,8 +103,8 @@ func (s *StructFieldsSuite) TestStructFields_SkippedFields_Unnamed() { if s.Assert().NotNil(unnamed) { lit := unnamed.Decl.(*ast.ValueSpec).Values[0].(*ast.CompositeLit) //nolint:forcetypeassert if s.Assert().NotNil(lit) { - s.Assert().Nil(sf.SkippedFields(lit, true)) - s.Assert().Nil(sf.SkippedFields(lit, false)) + s.Assert().Nil(sf.Skipped(lit, true)) + s.Assert().Nil(sf.Skipped(lit, false)) } } @@ -112,11 +112,11 @@ func (s *StructFieldsSuite) TestStructFields_SkippedFields_Unnamed() { if s.Assert().NotNil(unnamedIncomplete) { lit := unnamedIncomplete.Decl.(*ast.ValueSpec).Values[0].(*ast.CompositeLit) //nolint:forcetypeassert if s.Assert().NotNil(lit) { - s.Assert().Equal(fields.StructFields{ + s.Assert().Equal(structure.Fields{ {"unexportedRequired", false, false}, {"ExportedOptional", true, true}, {"unexportedOptional", false, true}, - }, sf.SkippedFields(lit, true)) + }, sf.Skipped(lit, true)) } } } @@ -128,8 +128,8 @@ func (s *StructFieldsSuite) TestStructFields_SkippedFields_Named() { if s.Assert().NotNil(named) { lit := named.Decl.(*ast.ValueSpec).Values[0].(*ast.CompositeLit) //nolint:forcetypeassert if s.Assert().NotNil(lit) { - s.Assert().Nil(sf.SkippedFields(lit, true)) - s.Assert().Nil(sf.SkippedFields(lit, false)) + s.Assert().Nil(sf.Skipped(lit, true)) + s.Assert().Nil(sf.Skipped(lit, false)) } } @@ -137,10 +137,10 @@ func (s *StructFieldsSuite) TestStructFields_SkippedFields_Named() { if s.Assert().NotNil(namedIncomplete1) { lit := namedIncomplete1.Decl.(*ast.ValueSpec).Values[0].(*ast.CompositeLit) //nolint:forcetypeassert if s.Assert().NotNil(lit) { - s.Assert().Nil(sf.SkippedFields(lit, true)) - s.Assert().Equal(fields.StructFields{ + s.Assert().Nil(sf.Skipped(lit, true)) + s.Assert().Equal(structure.Fields{ {"unexportedRequired", false, false}, - }, sf.SkippedFields(lit, false)) + }, sf.Skipped(lit, false)) } } @@ -148,13 +148,13 @@ func (s *StructFieldsSuite) TestStructFields_SkippedFields_Named() { if s.Assert().NotNil(namedIncomplete2) { lit := namedIncomplete2.Decl.(*ast.ValueSpec).Values[0].(*ast.CompositeLit) //nolint:forcetypeassert if s.Assert().NotNil(lit) { - s.Assert().Equal(fields.StructFields{ + s.Assert().Equal(structure.Fields{ {"ExportedRequired", true, false}, - }, sf.SkippedFields(lit, true)) - s.Assert().Equal(fields.StructFields{ + }, sf.Skipped(lit, true)) + s.Assert().Equal(structure.Fields{ {"ExportedRequired", true, false}, {"unexportedRequired", false, false}, - }, sf.SkippedFields(lit, false)) + }, sf.Skipped(lit, false)) } } } diff --git a/internal/fields/testdata/structs.go b/internal/structure/testdata/structs.go similarity index 100% rename from internal/fields/testdata/structs.go rename to internal/structure/testdata/structs.go