feat: many-to-one check, constrints check
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
47
relationsCheck/checkManyToOne.go
Normal file
47
relationsCheck/checkManyToOne.go
Normal file
@@ -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
|
||||
}
|
||||
29
relationsCheck/constraints.go
Normal file
29
relationsCheck/constraints.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
33
tests/testdata/src/relations_check/negative.go
vendored
33
tests/testdata/src/relations_check/negative.go
vendored
@@ -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
|
||||
}
|
||||
|
||||
27
tests/testdata/src/relations_check/positive.go
vendored
27
tests/testdata/src/relations_check/positive.go
vendored
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user