From 49878c333c0aac3e46bffe0fe4424ee45dce0d2d Mon Sep 17 00:00:00 2001 From: GogaCoder Date: Mon, 30 Dec 2024 21:15:15 +0700 Subject: [PATCH] feat: references check ... and fixes: parse fields without tags and other logical errors --- common/model.go | 22 ++--- common/modelsParser.go | 77 +++++++++++++++ common/resolveBaseType.go | 21 ++++ foreignKeyCheck/foreignKeyCheck.go | 1 + referencesCheck/referencesCheck.go | 96 ++++++------------- tests/reference_check_test.go | 12 +++ .../testdata/src/references_check/negative.go | 24 +++++ .../testdata/src/references_check/positive.go | 19 ++++ 8 files changed, 196 insertions(+), 76 deletions(-) create mode 100644 common/modelsParser.go create mode 100644 common/resolveBaseType.go create mode 100644 foreignKeyCheck/foreignKeyCheck.go create mode 100644 tests/reference_check_test.go create mode 100644 tests/testdata/src/references_check/negative.go create mode 100644 tests/testdata/src/references_check/positive.go diff --git a/common/model.go b/common/model.go index 16d6577..dd4175b 100644 --- a/common/model.go +++ b/common/model.go @@ -6,18 +6,18 @@ import ( ) type Field struct { - Name string - Type ast.Expr - Tags *string - Options []string // contains options like "autoCreateTime" or "null" - Params []string // contains params like "foreignKey:CustomerId" or "constrain:OnDelete:Cascade" - Position token.Pos - Comment string + Name string + Type ast.Expr + Tags *string + Options []string // contains options like "autoCreateTime" or "null" + Params []string // contains params like "foreignKey:CustomerId" or "constrain:OnDelete:Cascade" + Pos token.Pos + Comment string } type Model struct { - Name string - Fields map[string]Field - Position token.Pos - Comment string + Name string + Fields map[string]Field + Pos token.Pos + Comment string } diff --git a/common/modelsParser.go b/common/modelsParser.go new file mode 100644 index 0000000..8f8a5d0 --- /dev/null +++ b/common/modelsParser.go @@ -0,0 +1,77 @@ +package common + +import ( + "github.com/fatih/structtag" + "go/ast" + "golang.org/x/tools/go/analysis" + "strings" +) + +func ParseModels(pass *analysis.Pass, models *map[string]Model) { + for _, file := range pass.Files { + ast.Inspect(file, func(node ast.Node) bool { + typeSpec, ok := node.(*ast.TypeSpec) + if !ok { + return true + } + structure, ok := typeSpec.Type.(*ast.StructType) + if !ok { + return true + } + + if err := CheckUnnamedModel(*typeSpec); err != nil { + pass.Reportf(structure.Pos(), err.Error()) + return false + } + + var model Model + model.Name = typeSpec.Name.Name + model.Comment = typeSpec.Comment.Text() + model.Pos = structure.Pos() + model.Fields = make(map[string]Field) + + for _, field := range structure.Fields.List { + var structField Field + if err := CheckUnnamedField(typeSpec.Name.Name, *field); err != nil { + pass.Reportf(field.Pos(), err.Error()) + return false + } + + structField.Name = field.Names[0].Name + structField.Pos = field.Pos() + structField.Comment = field.Comment.Text() + structField.Type = field.Type + + if field.Tag != nil { + structField.Tags = &field.Tag.Value + + tags, err := structtag.Parse(NormalizeStructTags(field.Tag.Value)) + if err != nil { + pass.Reportf(field.Pos(), "Invalid structure tag: %s\n", err) + return false + } + if tags != nil { + gormTag, parseErr := tags.Get("gorm") + if gormTag != nil && parseErr == nil { + gormTag.Options = append([]string{gormTag.Name}, gormTag.Options...) + for _, opt := range gormTag.Options { + if strings.Contains(opt, ":") { + structField.Params = append(structField.Params, opt) + } else { + structField.Options = append(structField.Options, opt) + } + } + } + if parseErr != nil { + pass.Reportf(field.Pos(), "Invalid structure tag: %s\n", parseErr) + return false + } + } + } + model.Fields[structField.Name] = structField + (*models)[model.Name] = model + } + return false + }) + } +} diff --git a/common/resolveBaseType.go b/common/resolveBaseType.go new file mode 100644 index 0000000..cb0688a --- /dev/null +++ b/common/resolveBaseType.go @@ -0,0 +1,21 @@ +package common + +import ( + "go/ast" +) + +func ResolveBaseType(expr ast.Expr) *string { + switch e := expr.(type) { + case *ast.Ident: + return &e.Name + case *ast.StarExpr: + return ResolveBaseType(e.X) + case *ast.ArrayType: + return ResolveBaseType(e.Elt) + case *ast.SelectorExpr: + return ResolveBaseType(e.X) + case *ast.ParenExpr: + return ResolveBaseType(e.X) + } + return nil +} diff --git a/foreignKeyCheck/foreignKeyCheck.go b/foreignKeyCheck/foreignKeyCheck.go new file mode 100644 index 0000000..f1a93f3 --- /dev/null +++ b/foreignKeyCheck/foreignKeyCheck.go @@ -0,0 +1 @@ +package foreignKeyCheck diff --git a/referencesCheck/referencesCheck.go b/referencesCheck/referencesCheck.go index 9a34eaf..86be654 100644 --- a/referencesCheck/referencesCheck.go +++ b/referencesCheck/referencesCheck.go @@ -1,8 +1,7 @@ package referencesCheck import ( - "github.com/fatih/structtag" - "go/ast" + "fmt" "golang.org/x/tools/go/analysis" "gormlint/common" "strings" @@ -17,77 +16,44 @@ var ReferenceAnalyzer = &analysis.Analyzer{ var models map[string]common.Model -func init() { - models = make(map[string]common.Model) -} - func run(pass *analysis.Pass) (any, error) { - // TODO: move in new function - for _, file := range pass.Files { - ast.Inspect(file, func(node ast.Node) bool { - typeSpec, ok := node.(*ast.TypeSpec) - if !ok { - return true - } - structure, ok := typeSpec.Type.(*ast.StructType) - if !ok { - return true - } + models = make(map[string]common.Model) + common.ParseModels(pass, &models) - if err := common.CheckUnnamedModel(*typeSpec); err != nil { - pass.Reportf(structure.Pos(), err.Error()) - return false - } - - var model common.Model - model.Name = typeSpec.Name.Name - model.Comment = typeSpec.Comment.Text() - model.Position = structure.Pos() - model.Fields = make(map[string]common.Field) - - for _, field := range structure.Fields.List { - var structField common.Field - if err := common.CheckUnnamedField(typeSpec.Name.Name, *field); err != nil { - pass.Reportf(field.Pos(), err.Error()) - return false + for _, model := range models { + for _, field := range model.Fields { + for _, param := range field.Params { + pair := strings.Split(param, ":") + if len(pair) < 2 { + fmt.Printf("%s", param) } - structField.Name = field.Names[0].Name - structField.Position = field.Pos() - structField.Comment = field.Comment.Text() - structField.Type = field.Type - if field.Tag != nil { - structField.Tags = &field.Tag.Value + paramName := pair[0] + paramValue := pair[1] - tags, err := structtag.Parse(common.NormalizeStructTags(field.Tag.Value)) - if err != nil { - pass.Reportf(field.Pos(), "Invalid structure tag: %s\n", err) - return false - } - if tags != nil { - gormTag, parseErr := tags.Get("gorm") - if gormTag != nil && parseErr == nil { - gormTag.Options = append([]string{gormTag.Name}, gormTag.Options...) - for _, opt := range gormTag.Options { - if strings.Contains(opt, ":") { - structField.Params = append(structField.Options, opt) - } else { - structField.Options = append(structField.Options, opt) - } - } - } - if parseErr != nil { - pass.Reportf(field.Pos(), "Invalid structure tag: %s\n", parseErr) - return false - } + if paramName == "reference" { + pass.Reportf(field.Pos, "Typo in tag: \"reference\" instead of verb \"references\"") + } + if paramName == "references" { + fieldType := common.ResolveBaseType(field.Type) + + if fieldType == nil { + pass.Reportf(field.Pos, "Failed to process references check. Cannot resolve type \"%s\" in field \"%s\"", field.Type, field.Name) + return nil, nil } - model.Fields[structField.Name] = structField - } + relatedModel, modelExists := models[*fieldType] - models[model.Name] = model + if modelExists { + _, fieldExists := relatedModel.Fields[paramValue] + if !fieldExists { + pass.Reportf(field.Pos, "Related field \"%s\" doesn't exist on model \"%s\"", paramValue, relatedModel.Name) + } + } else { + pass.Reportf(field.Pos, "Related model \"%s\" doesn't exist", *fieldType) + } + } } - return false - }) + } } return nil, nil } diff --git a/tests/reference_check_test.go b/tests/reference_check_test.go new file mode 100644 index 0000000..72e103a --- /dev/null +++ b/tests/reference_check_test.go @@ -0,0 +1,12 @@ +package tests + +import ( + "golang.org/x/tools/go/analysis/analysistest" + "gormlint/referencesCheck" + "testing" +) + +func TestReferenceCheck(t *testing.T) { + t.Parallel() + analysistest.Run(t, analysistest.TestData(), referencesCheck.ReferenceAnalyzer, "references_check") +} diff --git a/tests/testdata/src/references_check/negative.go b/tests/testdata/src/references_check/negative.go new file mode 100644 index 0000000..f3bd40d --- /dev/null +++ b/tests/testdata/src/references_check/negative.go @@ -0,0 +1,24 @@ +package references_check + +type WorkArea struct { + Id uint `gorm:"primaryKey"` + Workshop Workshop `gorm:"foreignKey:WorkshopId;references:Id;"` + WorkshopId uint +} + +type Workshop struct { + Id uint `gorm:"primaryKey"` + Name string + WorkAreas []WorkArea `gorm:"constraint:OnDelete:CASCADE;"` +} + +type TeamType struct { + Code uint `gorm:"primaryKey"` + Name string `gorm:"not null"` +} + +type TeamTask struct { + Id uint `gorm:"primaryKey"` + TeamTypeId uint + TeamType TeamType `gorm:"references:Code;"` +} diff --git a/tests/testdata/src/references_check/positive.go b/tests/testdata/src/references_check/positive.go new file mode 100644 index 0000000..7efde8c --- /dev/null +++ b/tests/testdata/src/references_check/positive.go @@ -0,0 +1,19 @@ +package references_check + +type User struct { + Name string + CompanyID string + Company Company `gorm:"references:code"` // want "Related field \"code\" doesn't exist on model \"Company\"" +} + +type Company struct { + ID int + Code string + Name string +} + +type Order struct { + Id uint `gorm:"primaryKey"` + CompanyID string `gorm:"references:Code"` // want "Related model \"string\" doesn't exist" + Company Company +}