diff --git a/common/model_methods.go b/common/model_methods.go index d87d246..75ea1a1 100644 --- a/common/model_methods.go +++ b/common/model_methods.go @@ -17,3 +17,12 @@ func (model *Model) HasField(name string) bool { } return false } + +func (model *Model) PrimaryKey() bool { + for _, field := range model.Fields { + if field.Tags.HasOption("primaryKey") { + return true + } + } + return false +} diff --git a/common/modelsParser.go b/common/modelsParser.go index 88e2fcc..9172857 100644 --- a/common/modelsParser.go +++ b/common/modelsParser.go @@ -64,6 +64,11 @@ func ParseModels(pass *analysis.Pass, models *map[string]Model) { model.Fields[structField.Name] = structField (*models)[model.Name] = model } + + if _, exist := model.Fields["Id"]; !exist { + pass.Reportf(model.Pos, "Id field should be presented model \"%s\"", model.Name) + } + return false }) } diff --git a/relationsCheck/checkBelongsTo.go b/relationsCheck/checkBelongsTo.go new file mode 100644 index 0000000..a67389d --- /dev/null +++ b/relationsCheck/checkBelongsTo.go @@ -0,0 +1,19 @@ +package relationsCheck + +import ( + "gormlint/common" +) + +func IsBelongsTo(field common.Field, model common.Model, relatedModel common.Model) bool { + foreignKey := field.Tags.GetParamOr("foreignKey", "Id") + references := field.Tags.GetParamOr("references", relatedModel.Name+"Id") + + if !model.HasField(references) { + return false + } + if !relatedModel.HasField(foreignKey) { + return false + } + + return true +} diff --git a/relationsCheck/checkManyToOne.go b/relationsCheck/checkManyToOne.go index e150ea5..4dfee54 100644 --- a/relationsCheck/checkManyToOne.go +++ b/relationsCheck/checkManyToOne.go @@ -1,15 +1,25 @@ package relationsCheck import ( + "go/token" "go/types" "golang.org/x/tools/go/analysis" "gormlint/common" "strings" ) +var alreadyReported = make(map[token.Pos]bool) + func checkManyToOne(pass *analysis.Pass, nestedField common.Field, model common.Model, relatedModel common.Model) bool { + /* Return true, if found problems */ + foreignKey := nestedField.Tags.GetParamOr("foreignKey", model.Name+"Id") references := nestedField.Tags.GetParamOr("references", "Id") + + if alreadyReported[nestedField.Pos] { + return true + } + if !relatedModel.HasField(foreignKey) { pass.Reportf( nestedField.Pos, @@ -18,8 +28,10 @@ func checkManyToOne(pass *analysis.Pass, nestedField common.Field, model common. relatedModel.Name, model.Name, ) + alreadyReported[nestedField.Pos] = true return true } + if !model.HasField(references) { pass.Reportf( nestedField.Pos, @@ -28,20 +40,31 @@ func checkManyToOne(pass *analysis.Pass, nestedField common.Field, model common. 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) + alreadyReported[nestedField.Pos] = true return true } - if !strings.Contains(referencesType, "int") { - pass.Reportf(model.Fields[references].Pos, "References key `%s` has invalid type", referencesType) + foreignKeyField := relatedModel.Fields[foreignKey] + referencesField := model.Fields[references] + foreignKeyType := types.ExprString(foreignKeyField.Type) + referencesType := types.ExprString(referencesField.Type) + + if alreadyReported[foreignKeyField.Pos] || alreadyReported[referencesField.Pos] { return true } - + + if !strings.Contains(foreignKeyType, "int") && !alreadyReported[foreignKeyField.Pos] { + // TODO: process UUID as foreign key type + pass.Reportf(foreignKeyField.Pos, "Foreign key `%s` has invalid type", foreignKeyType) + alreadyReported[foreignKeyField.Pos] = true + return true + } + + if !strings.Contains(referencesType, "int") && !alreadyReported[referencesField.Pos] { + pass.Reportf(referencesField.Pos, "References key `%s` has invalid type", referencesType) + alreadyReported[referencesField.Pos] = true + return true + } + return false } diff --git a/relationsCheck/checkOneToMany.go b/relationsCheck/checkOneToMany.go new file mode 100644 index 0000000..ac7b44f --- /dev/null +++ b/relationsCheck/checkOneToMany.go @@ -0,0 +1,42 @@ +package relationsCheck + +import ( + "fmt" + "go/types" + "golang.org/x/tools/go/analysis" + "gormlint/common" +) + +func findBackReferenceInOneToMany(model common.Model, relatedModel common.Model) *common.Field { + for _, field := range relatedModel.Fields { + if !common.IsSlice(field.Type) { + continue + } + if field.Tags.HasParam("many2many") { + continue + } + baseType := common.ResolveBaseType(field.Type) + if baseType == nil { + continue + } + if *baseType == model.Name { + return &field + } + } + return nil +} + +func isOneToMany(pass *analysis.Pass, model common.Model, relatedModel common.Model) bool { + backReference := findBackReferenceInOneToMany(model, relatedModel) + if backReference == nil { + return false + } + fmt.Println("Found back reference") + fmt.Printf("Backref type: %s\n", types.ExprString(backReference.Type)) + fmt.Printf("Model: %s\n", model.Name) + fmt.Printf("Related model: %s\n", relatedModel.Name) + if checkManyToOne(pass, *backReference, relatedModel, model) { + return false + } + return true +} diff --git a/relationsCheck/relationsCheck.go b/relationsCheck/relationsCheck.go index ea309e0..9cb21fc 100644 --- a/relationsCheck/relationsCheck.go +++ b/relationsCheck/relationsCheck.go @@ -48,12 +48,17 @@ func CheckTypesOfM2M(pass *analysis.Pass, modelName string, relatedModelName str } func CheckMany2Many(pass *analysis.Pass, models map[string]common.Model) { - // TODO: unexpected duplicated relations - var knownModels []string + var processedRelations []string + for _, model := range models { for _, field := range model.Fields { m2mRelation := field.Tags.GetParam("many2many") if m2mRelation != nil { + if slices.Contains(processedRelations, m2mRelation.Value) { + continue + } + processedRelations = append(processedRelations, m2mRelation.Value) + relatedModel := common.GetModelFromType(field.Type, models) if relatedModel == nil { pass.Reportf(field.Pos, "Failed to resolve related model type") @@ -62,46 +67,21 @@ func CheckMany2Many(pass *analysis.Pass, models map[string]common.Model) { backReference := common.FindBackReferenceInM2M(m2mRelation.Value, *relatedModel) if backReference != nil { - if slices.Contains(knownModels, relatedModel.Name) { - continue - } else { - knownModels = append(knownModels, model.Name) - knownModels = append(knownModels, relatedModel.Name) - } 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 { - CheckTypesOfM2M(pass, model.Name, relatedModel.Name, m2mRelation.Value, field, field) - } else { - 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) + if CheckTypesOfM2M(pass, model.Name, relatedModel.Name, m2mRelation.Value, field, field) { continue } - } - // Here you can forbid M2M relations without back-reference - // TODO: process m2m without backref - if CheckCascadeDelete(pass, field) { - continue - } - } - } else { - 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 + } else { + pass.Reportf(field.Pos, "M2M relation `%s` missing back-reference in model `%s`", m2mRelation.Value, relatedModel.Name) } if CheckCascadeDelete(pass, field) { continue @@ -112,10 +92,42 @@ func CheckMany2Many(pass *analysis.Pass, models map[string]common.Model) { } } +func CheckOneToMany(pass *analysis.Pass, models map[string]common.Model) { + for _, model := range models { + for _, field := range model.Fields { + if common.IsSlice(field.Type) { + continue + } + + baseType := common.ResolveBaseType(field.Type) + if baseType == nil { + pass.Reportf(field.Pos, "Failed to resolve field base type: `%s`", field.Type) + continue + } + relatedModel := common.GetModelFromType(field.Type, models) + if relatedModel == nil { + continue + } + + foundOneToMany := isOneToMany(pass, model, *relatedModel) + if foundOneToMany { + fmt.Printf("Found 1:M relation in model `%s` with model `%s`\n", model.Name, *baseType) + } + + if !foundOneToMany { + foundBelongsTo := IsBelongsTo(field, model, *relatedModel) + if foundBelongsTo { + fmt.Printf("Found belongs to relation in model `%s` with model `%s`\n", model.Name, *baseType) + } + } + } + } +} + func run(pass *analysis.Pass) (any, error) { models := make(map[string]common.Model) common.ParseModels(pass, &models) CheckMany2Many(pass, models) - + CheckOneToMany(pass, models) return nil, nil } diff --git a/tests/testdata/src/relations_check/negative.go b/tests/testdata/src/relations_check/negative.go index 449c4de..b14d80d 100644 --- a/tests/testdata/src/relations_check/negative.go +++ b/tests/testdata/src/relations_check/negative.go @@ -28,6 +28,7 @@ type Writer struct { } // One-to-many + type Comment struct { Id uint `gorm:"primaryKey"` CommentatorId uint diff --git a/tests/testdata/src/relations_check/positive.go b/tests/testdata/src/relations_check/positive.go index 63f0256..67f4e27 100644 --- a/tests/testdata/src/relations_check/positive.go +++ b/tests/testdata/src/relations_check/positive.go @@ -30,8 +30,8 @@ type City struct { Kuzbass Kuzbass } -type Federation struct { - Lands []Land `gorm:"constraint:OnDelete:CASCADE;"` // want "Expected references `Id` in model `Federation` for 1:M relation with model `Land`" +type Federation struct { // want "Id field should be presented model \"Federation\"" + Lands []Land `gorm:"constraint:OnDelete:CASCADE;"` } type Land struct {