feat: one-to-many, belongs-to

This commit is contained in:
2025-03-17 23:47:01 +07:00
parent db9ef0f935
commit 9d14fa7c57
8 changed files with 156 additions and 45 deletions

View File

@@ -17,3 +17,12 @@ func (model *Model) HasField(name string) bool {
} }
return false return false
} }
func (model *Model) PrimaryKey() bool {
for _, field := range model.Fields {
if field.Tags.HasOption("primaryKey") {
return true
}
}
return false
}

View File

@@ -64,6 +64,11 @@ func ParseModels(pass *analysis.Pass, models *map[string]Model) {
model.Fields[structField.Name] = structField model.Fields[structField.Name] = structField
(*models)[model.Name] = model (*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 return false
}) })
} }

View File

@@ -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
}

View File

@@ -1,15 +1,25 @@
package relationsCheck package relationsCheck
import ( import (
"go/token"
"go/types" "go/types"
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"gormlint/common" "gormlint/common"
"strings" "strings"
) )
var alreadyReported = make(map[token.Pos]bool)
func checkManyToOne(pass *analysis.Pass, nestedField common.Field, model common.Model, relatedModel common.Model) 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") foreignKey := nestedField.Tags.GetParamOr("foreignKey", model.Name+"Id")
references := nestedField.Tags.GetParamOr("references", "Id") references := nestedField.Tags.GetParamOr("references", "Id")
if alreadyReported[nestedField.Pos] {
return true
}
if !relatedModel.HasField(foreignKey) { if !relatedModel.HasField(foreignKey) {
pass.Reportf( pass.Reportf(
nestedField.Pos, nestedField.Pos,
@@ -18,8 +28,10 @@ func checkManyToOne(pass *analysis.Pass, nestedField common.Field, model common.
relatedModel.Name, relatedModel.Name,
model.Name, model.Name,
) )
alreadyReported[nestedField.Pos] = true
return true return true
} }
if !model.HasField(references) { if !model.HasField(references) {
pass.Reportf( pass.Reportf(
nestedField.Pos, nestedField.Pos,
@@ -28,20 +40,31 @@ func checkManyToOne(pass *analysis.Pass, nestedField common.Field, model common.
model.Name, model.Name,
relatedModel.Name, relatedModel.Name,
) )
return true alreadyReported[nestedField.Pos] = 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 return true
} }
if !strings.Contains(referencesType, "int") { foreignKeyField := relatedModel.Fields[foreignKey]
pass.Reportf(model.Fields[references].Pos, "References key `%s` has invalid type", referencesType) referencesField := model.Fields[references]
foreignKeyType := types.ExprString(foreignKeyField.Type)
referencesType := types.ExprString(referencesField.Type)
if alreadyReported[foreignKeyField.Pos] || alreadyReported[referencesField.Pos] {
return true 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 return false
} }

View File

@@ -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
}

View File

@@ -48,12 +48,17 @@ func CheckTypesOfM2M(pass *analysis.Pass, modelName string, relatedModelName str
} }
func CheckMany2Many(pass *analysis.Pass, models map[string]common.Model) { func CheckMany2Many(pass *analysis.Pass, models map[string]common.Model) {
// TODO: unexpected duplicated relations var processedRelations []string
var knownModels []string
for _, model := range models { for _, model := range models {
for _, field := range model.Fields { for _, field := range model.Fields {
m2mRelation := field.Tags.GetParam("many2many") m2mRelation := field.Tags.GetParam("many2many")
if m2mRelation != nil { if m2mRelation != nil {
if slices.Contains(processedRelations, m2mRelation.Value) {
continue
}
processedRelations = append(processedRelations, m2mRelation.Value)
relatedModel := common.GetModelFromType(field.Type, models) relatedModel := common.GetModelFromType(field.Type, models)
if relatedModel == nil { if relatedModel == nil {
pass.Reportf(field.Pos, "Failed to resolve related model type") 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) backReference := common.FindBackReferenceInM2M(m2mRelation.Value, *relatedModel)
if backReference != nil { 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) { if CheckTypesOfM2M(pass, model.Name, relatedModel.Name, m2mRelation.Value, field, *backReference) {
continue 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) { if CheckCascadeDelete(pass, field) {
continue continue
} }
} else { } else {
// Check self-reference // Обработка самоссылки
if model.Name == relatedModel.Name { if model.Name == relatedModel.Name {
CheckTypesOfM2M(pass, model.Name, relatedModel.Name, m2mRelation.Value, field, field) if 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)
continue continue
} }
} } else {
// Here you can forbid M2M relations without back-reference pass.Reportf(field.Pos, "M2M relation `%s` missing back-reference in model `%s`", m2mRelation.Value, relatedModel.Name)
// 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
} }
if CheckCascadeDelete(pass, field) { if CheckCascadeDelete(pass, field) {
continue 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) { func run(pass *analysis.Pass) (any, error) {
models := make(map[string]common.Model) models := make(map[string]common.Model)
common.ParseModels(pass, &models) common.ParseModels(pass, &models)
CheckMany2Many(pass, models) CheckMany2Many(pass, models)
CheckOneToMany(pass, models)
return nil, nil return nil, nil
} }

View File

@@ -28,6 +28,7 @@ type Writer struct {
} }
// One-to-many // One-to-many
type Comment struct { type Comment struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
CommentatorId uint CommentatorId uint

View File

@@ -30,8 +30,8 @@ type City struct {
Kuzbass Kuzbass Kuzbass Kuzbass
} }
type Federation struct { type Federation struct { // want "Id field should be presented model \"Federation\""
Lands []Land `gorm:"constraint:OnDelete:CASCADE;"` // want "Expected references `Id` in model `Federation` for 1:M relation with model `Land`" Lands []Land `gorm:"constraint:OnDelete:CASCADE;"`
} }
type Land struct { type Land struct {