From 60e0a64a24ef88aea51c88e4282cafddf69d5ca4 Mon Sep 17 00:00:00 2001 From: gogacoder Date: Mon, 17 Mar 2025 13:13:35 +0700 Subject: [PATCH] feat: many-to-one check, constrints check --- common/model_methods.go | 9 ++++ relationsCheck/checkManyToOne.go | 47 +++++++++++++++++++ relationsCheck/constraints.go | 29 ++++++++++++ relationsCheck/relationsCheck.go | 41 ++++++++++++---- .../testdata/src/relations_check/negative.go | 33 +++++++++++-- .../testdata/src/relations_check/positive.go | 27 +++++++++-- 6 files changed, 168 insertions(+), 18 deletions(-) create mode 100644 relationsCheck/checkManyToOne.go create mode 100644 relationsCheck/constraints.go diff --git a/common/model_methods.go b/common/model_methods.go index 1d60307..d87d246 100644 --- a/common/model_methods.go +++ b/common/model_methods.go @@ -8,3 +8,12 @@ func (model *Model) HasPrimaryKey() bool { } return false } + +func (model *Model) HasField(name string) bool { + for _, field := range model.Fields { + if field.Name == name { + return true + } + } + return false +} diff --git a/relationsCheck/checkManyToOne.go b/relationsCheck/checkManyToOne.go new file mode 100644 index 0000000..e150ea5 --- /dev/null +++ b/relationsCheck/checkManyToOne.go @@ -0,0 +1,47 @@ +package relationsCheck + +import ( + "go/types" + "golang.org/x/tools/go/analysis" + "gormlint/common" + "strings" +) + +func checkManyToOne(pass *analysis.Pass, nestedField common.Field, model common.Model, relatedModel common.Model) bool { + foreignKey := nestedField.Tags.GetParamOr("foreignKey", model.Name+"Id") + references := nestedField.Tags.GetParamOr("references", "Id") + if !relatedModel.HasField(foreignKey) { + pass.Reportf( + nestedField.Pos, + "Expected foreignKey `%s` in model `%s` for 1:M relation with model `%s`", + foreignKey, + relatedModel.Name, + model.Name, + ) + return true + } + if !model.HasField(references) { + pass.Reportf( + nestedField.Pos, + "Expected references `%s` in model `%s` for 1:M relation with model `%s`", + references, + model.Name, + relatedModel.Name, + ) + return true + } + foreignKeyType := types.ExprString(relatedModel.Fields[foreignKey].Type) + referencesType := types.ExprString(model.Fields[references].Type) + if !strings.Contains(foreignKeyType, "int") { + // TODO: process UUID as foreign key type + pass.Reportf(relatedModel.Fields[foreignKey].Pos, "Foreign key `%s` has invalid type", foreignKeyType) + return true + } + + if !strings.Contains(referencesType, "int") { + pass.Reportf(model.Fields[references].Pos, "References key `%s` has invalid type", referencesType) + return true + } + + return false +} diff --git a/relationsCheck/constraints.go b/relationsCheck/constraints.go new file mode 100644 index 0000000..5af74e6 --- /dev/null +++ b/relationsCheck/constraints.go @@ -0,0 +1,29 @@ +package relationsCheck + +import ( + "golang.org/x/tools/go/analysis" + "gormlint/common" + "strings" +) + +func CheckCascadeDelete(pass *analysis.Pass, field common.Field) bool { + if !field.Tags.HasParam("constraint") { + pass.Reportf(field.Pos, "field %s should have a constraint", field.Name) + return true + } + constraintValue := field.Tags.GetParam("constraint").Value + pair := strings.Split(constraintValue, ":") + trigger, value := pair[0], pair[1] + if strings.ToLower(trigger) == "OnDelete" && strings.ToUpper(value) != "CASCADE" { + pass.Reportf(field.Pos, "field have invalid constraint on `OnDelete trigger`") + return true + } + + if field.Tags.HasParam("OnDelete") { + if strings.ToUpper(field.Tags.GetParam("OnDelete").Value) != "CASCADE" { + pass.Reportf(field.Pos, "field have invalid constraint on `OnDelete` trigger") + return true + } + } + return false +} diff --git a/relationsCheck/relationsCheck.go b/relationsCheck/relationsCheck.go index 0d8737a..ea309e0 100644 --- a/relationsCheck/relationsCheck.go +++ b/relationsCheck/relationsCheck.go @@ -14,35 +14,37 @@ var RelationsAnalyzer = &analysis.Analyzer{ Run: run, } -func CheckTypesOfM2M(pass *analysis.Pass, modelName string, relatedModelName string, relationName string, reference common.Field, backReference common.Field) { +func CheckTypesOfM2M(pass *analysis.Pass, modelName string, relatedModelName string, relationName string, reference common.Field, backReference common.Field) bool { if !common.IsSlice(reference.Type) { pass.Reportf(reference.Pos, "M2M relation `%s` with bad type `%s` (should be a slice)", relationName, reference.Type) - return + return true } if !common.IsSlice(backReference.Type) { pass.Reportf(backReference.Pos, "M2M relation `%s` with bad type `%s` (should be a slice)", relationName, backReference.Type) - return + return true } referenceBaseType := common.ResolveBaseType(reference.Type) if referenceBaseType == nil { pass.Reportf(reference.Pos, "Failed to resolve field type: `%s`", reference.Type) - return + return true } backReferenceBaseType := common.ResolveBaseType(backReference.Type) if backReferenceBaseType == nil { pass.Reportf(reference.Pos, "Failed to resolve type: `%s`", reference.Type) - return + return true } if *backReferenceBaseType != modelName { pass.Reportf(backReference.Pos, "Invalid type `%s` in M2M relation (use []*%s or self-reference)", *backReferenceBaseType, modelName) - return + return true } if *referenceBaseType != relatedModelName { pass.Reportf(reference.Pos, "Invalid type `%s` in M2M relation (use []*%s or self-reference)", *referenceBaseType, relatedModelName) + return true } + return false } func CheckMany2Many(pass *analysis.Pass, models map[string]common.Model) { @@ -55,7 +57,7 @@ func CheckMany2Many(pass *analysis.Pass, models map[string]common.Model) { relatedModel := common.GetModelFromType(field.Type, models) if relatedModel == nil { pass.Reportf(field.Pos, "Failed to resolve related model type") - return + continue } backReference := common.FindBackReferenceInM2M(m2mRelation.Value, *relatedModel) @@ -66,9 +68,14 @@ func CheckMany2Many(pass *analysis.Pass, models map[string]common.Model) { knownModels = append(knownModels, model.Name) knownModels = append(knownModels, relatedModel.Name) } - CheckTypesOfM2M(pass, model.Name, relatedModel.Name, m2mRelation.Value, field, *backReference) + if CheckTypesOfM2M(pass, model.Name, relatedModel.Name, m2mRelation.Value, field, *backReference) { + continue + } // TODO: check foreign key and references fmt.Printf("Found M2M relation between \"%s\" and \"%s\"\n", model.Name, relatedModel.Name) + if CheckCascadeDelete(pass, field) { + continue + } } else { // Check self-reference if model.Name == relatedModel.Name { @@ -77,13 +84,29 @@ func CheckMany2Many(pass *analysis.Pass, models map[string]common.Model) { if !relatedModel.HasPrimaryKey() { fmt.Printf("%#v\n", relatedModel) pass.Reportf(field.Pos, "Can't build M2M relation `%s`, primary key on `%s` model is absont", m2mRelation.Value, relatedModel.Name) + continue } } // Here you can forbid M2M relations without back-reference // TODO: process m2m without backref + if CheckCascadeDelete(pass, field) { + continue + } } } else { - // TODO: check [] and process m:1 + if common.IsSlice(field.Type) { + relatedModel := common.GetModelFromType(field.Type, models) + if relatedModel == nil { + pass.Reportf(field.Pos, "Failed to resolve related model type") + continue + } + if checkManyToOne(pass, field, model, *relatedModel) { + continue + } + if CheckCascadeDelete(pass, field) { + continue + } + } } } } diff --git a/tests/testdata/src/relations_check/negative.go b/tests/testdata/src/relations_check/negative.go index 9b0c0b9..449c4de 100644 --- a/tests/testdata/src/relations_check/negative.go +++ b/tests/testdata/src/relations_check/negative.go @@ -4,25 +4,48 @@ package relations_check type Library struct { Id uint `gorm:"primaryKey"` - Books []*Book `gorm:"many2many:library_book;"` + Books []*Book `gorm:"many2many:library_book;constraint:OnDelete:CASCADE;"` } type Book struct { Id uint `gorm:"primaryKey"` - Libraries []*Library `gorm:"many2many:library_book;"` + Libraries []*Library `gorm:"many2many:library_book;constraint:OnDelete:CASCADE;"` } type Employee struct { Id uint `gorm:"primaryKey"` - Subordinates []*Employee `gorm:"many2many:employee_subordinates;"` // self-reference + Subordinates []*Employee `gorm:"many2many:employee_subordinates;constraint:OnDelete:CASCADE;"` // self-reference } type Publisher struct { Id uint `gorm:"primaryKey"` - Writers []*Writer `gorm:"many2many:publisher_books;"` + Writers []*Writer `gorm:"many2many:publisher_books;constraint:OnDelete:CASCADE;"` } type Writer struct { Id uint `gorm:"primaryKey"` - Publishers []Publisher `gorm:"many2many:publisher_books;"` + Publishers []Publisher `gorm:"many2many:publisher_books;constraint:OnDelete:CASCADE;"` +} + +// One-to-many +type Comment struct { + Id uint `gorm:"primaryKey"` + CommentatorId uint + Commentator Commentator +} + +type Commentator struct { + Id uint `gorm:"primaryKey"` + Comments []Comment `gorm:"foreignKey:CommentatorId;references:Id;constraint:OnDelete:CASCADE;"` +} + +type Post struct { + Id uint `gorm:"primaryKey"` + Files []*File `gorm:"constraint:OnDelete:CASCADE;"` +} + +type File struct { + Id uint `gorm:"primaryKey"` + PostId uint + Post Post } diff --git a/tests/testdata/src/relations_check/positive.go b/tests/testdata/src/relations_check/positive.go index 1de481a..0b0fbb2 100644 --- a/tests/testdata/src/relations_check/positive.go +++ b/tests/testdata/src/relations_check/positive.go @@ -2,20 +2,39 @@ package relations_check type Student struct { Id uint `gorm:"primaryKey"` - Courses []Course `gorm:"many2many:student_courses;"` + Courses []Course `gorm:"many2many:student_courses;constraint:OnDelete:CASCADE;"` } type Course struct { Id uint `gorm:"primaryKey"` - Students []Course `gorm:"many2many:student_courses;"` // want "Invalid type `Course` in M2M relation \\(use \\[\\]\\*Student or self-reference\\)" + Students []Course `gorm:"many2many:student_courses;constraint:OnDelete:CASCADE"` // want "Invalid type `Course` in M2M relation \\(use \\[\\]\\*Student or self-reference\\)" } type Author struct { Id uint `gorm:"primaryKey"` - Articles []Article `gorm:"many2many:author_articles;"` + Articles []Article `gorm:"many2many:author_articles;constraint:OnDelete:CASCADES;"` } type Article struct { Id uint `gorm:"primaryKey"` - Authors Author `gorm:"many2many:author_articles;"` // want "M2M relation `author_articles` with bad type `Author` \\(should be a slice\\)" + Authors Author `gorm:"many2many:author_articles;constraint:OnDelete:CASCADE;"` // want "M2M relation `author_articles` with bad type `Author` \\(should be a slice\\)" +} + +type Kuzbass struct { + Id uint `gorm:"primaryKey"` + Cities []City // want "Expected foreignKey `KuzbassId` in model `City` for 1:M relation with model `Kuzbass`" +} + +type City struct { + Id uint `gorm:"primaryKey"` + Kuzbass Kuzbass +} + +type Federation struct { + Lands []Land `gorm:"constraint:OnDelete:CASCADE;"` // want "" +} + +type Land struct { + Id uint `gorm:"primaryKey"` + FederationId uint }