feat: foreignKeys check

+fix: "unnamed" field bug
This commit is contained in:
2024-12-30 23:44:24 +07:00
parent ece3f3c801
commit 03ccaff375
10 changed files with 123 additions and 21 deletions

View File

@@ -32,12 +32,18 @@ func ParseModels(pass *analysis.Pass, models *map[string]Model) {
for _, field := range structure.Fields.List { for _, field := range structure.Fields.List {
var structField Field var structField Field
if err := CheckUnnamedField(typeSpec.Name.Name, *field); err != nil {
pass.Reportf(field.Pos(), err.Error()) if len(field.Names) == 0 {
return false fieldType := ResolveBaseType(field.Type)
if fieldType == nil {
pass.Reportf(field.Pos(), "Failed to resolve model \"%s\" field type: %s", model.Name, field.Type)
} else {
structField.Name = *fieldType
}
} else {
structField.Name = field.Names[0].Name
} }
structField.Name = field.Names[0].Name
structField.Pos = field.Pos() structField.Pos = field.Pos()
structField.Comment = field.Comment.Text() structField.Comment = field.Comment.Text()
structField.Type = field.Type structField.Type = field.Type

View File

@@ -43,7 +43,7 @@ func isGormValueNullable(tags *structtag.Tags) (*bool, error) {
} }
} }
func CheckFieldNullConsistency(field ast.Field, structName string, structTags string) error { func CheckFieldNullConsistency(field ast.Field, fieldName string, structName string, structTags string) error {
tags, err := structtag.Parse(structTags) tags, err := structtag.Parse(structTags)
if err != nil { if err != nil {
return errors.New(fmt.Sprintf("Invalid structure tag: %s", err)) return errors.New(fmt.Sprintf("Invalid structure tag: %s", err))
@@ -64,7 +64,7 @@ func CheckFieldNullConsistency(field ast.Field, structName string, structTags st
} }
if isFieldNullable != *isColumnNullable { if isFieldNullable != *isColumnNullable {
return errors.New(fmt.Sprintf("Null safety error in \"%s\" model, field \"%s\": column nullable policy doesn't match to tag nullable policy", structName, field.Names[0].Name)) return errors.New(fmt.Sprintf("Null safety error in \"%s\" model, field \"%s\": column nullable policy doesn't match to tag nullable policy", structName, fieldName))
} }
return nil return nil
} }

View File

@@ -12,10 +12,3 @@ func CheckUnnamedModel(typeSpec ast.TypeSpec) error {
} }
return nil return nil
} }
func CheckUnnamedField(structName string, field ast.Field) error {
if len(field.Names) == 0 {
return errors.New(fmt.Sprintf("Struct \"%s\" has unnamed field", structName))
}
return nil
}

View File

@@ -1 +1,49 @@
package foreignKeyCheck package foreignKeyCheck
import (
"golang.org/x/tools/go/analysis"
"gormlint/common"
"strings"
)
// ForeignKeyCheck todo: add URL for foreign key analyzer rules
var ForeignKeyCheck = &analysis.Analyzer{
Name: "GormForeignKeyCheck",
Doc: "Check foreign key in gorm model struct tag",
Run: run,
}
var models map[string]common.Model
func run(pass *analysis.Pass) (any, error) {
models = make(map[string]common.Model)
common.ParseModels(pass, &models)
for _, model := range models {
for _, field := range model.Fields {
for _, param := range field.Params {
pair := strings.Split(param, ":")
paramName := pair[0]
paramValue := pair[1]
if paramName == "foreignKey" {
foreignKey, fieldExist := model.Fields[paramValue]
if !fieldExist {
pass.Reportf(field.Pos, "Foreign key \"%s\" mentioned in tag at field \"%s\" doesn't exist in model \"%s\"", paramValue, field.Name, model.Name)
} else {
foreignKeyType := common.ResolveBaseType(foreignKey.Type)
if foreignKeyType == nil {
pass.Reportf(foreignKey.Pos, "Failed to resolve type of foreign key field \"%s\": %s", field.Name, foreignKey.Type)
} else {
// TODO: handle all int types
if *foreignKeyType != "uint" && *foreignKeyType != "int" {
pass.Reportf(foreignKey.Pos, "Foreign key should have type like int, not \"%s\"", foreignKey.Type)
}
}
}
}
}
}
}
return nil, nil
}

View File

@@ -32,20 +32,26 @@ func run(pass *analysis.Pass) (any, error) {
} }
for _, field := range structure.Fields.List { for _, field := range structure.Fields.List {
if err := common.CheckUnnamedField(typeSpec.Name.Name, *field); err != nil { var structFieldName string
pass.Reportf(field.Pos(), err.Error()) if len(field.Names) == 0 {
return false fieldType := common.ResolveBaseType(field.Type)
if fieldType == nil {
pass.Reportf(field.Pos(), "Failed to resolve model \"%s\" field type: %s", typeSpec.Name.Name, field.Type)
} else {
structFieldName = *fieldType
}
} else {
structFieldName = field.Names[0].Name
} }
if field.Tag != nil { if field.Tag != nil {
tagWithoutQuotes := field.Tag.Value[1 : len(field.Tag.Value)-1] tagWithoutQuotes := field.Tag.Value[1 : len(field.Tag.Value)-1]
tagWithoutSemicolons := strings.ReplaceAll(tagWithoutQuotes, ";", ",") tagWithoutSemicolons := strings.ReplaceAll(tagWithoutQuotes, ";", ",")
err := common.CheckFieldNullConsistency(*field, typeSpec.Name.Name, tagWithoutSemicolons) err := common.CheckFieldNullConsistency(*field, structFieldName, typeSpec.Name.Name, tagWithoutSemicolons)
if err != nil { if err != nil {
pass.Reportf(field.Pos(), err.Error()) pass.Reportf(field.Pos(), err.Error())
return false return false
} }
} }
// TODO: check necessary tags for some fields
} }
return false return false
}) })

View File

@@ -13,10 +13,8 @@ var ReferenceAnalyzer = &analysis.Analyzer{
Run: run, Run: run,
} }
var models map[string]common.Model
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)
for _, model := range models { for _, model := range models {

View File

@@ -0,0 +1,12 @@
package tests
import (
"golang.org/x/tools/go/analysis/analysistest"
"gormlint/foreignKeyCheck"
"testing"
)
func TestForeignKeyCheck(t *testing.T) {
t.Parallel()
analysistest.Run(t, analysistest.TestData(), foreignKeyCheck.ForeignKeyCheck, "foreign_key_check")
}

View File

@@ -0,0 +1,12 @@
package foreign_key_check
type User struct {
Name string
CompanyRefer uint
Company Company `gorm:"foreignKey:CompanyRefer"`
}
type Company struct {
ID int
Name string
}

View File

@@ -0,0 +1,27 @@
package foreign_key_check
type PrepTask struct {
Id uint `gorm:"primaryKey"`
Description string
TaskId uint
WorkAreaId uint
WorkArea `gorm:"foreignKey:WorkAreaIds;constraint:OnDelete:CASCADE;"` // want "Foreign key \"WorkAreaIds\" mentioned in tag at field \"WorkArea\" doesn't exist in model \"PrepTask\""
Deadline int64
}
type WorkArea struct {
Id uint `gorm:"primaryKey"`
Name string
Description string
Performance uint
PrepTasks []PrepTask `gorm:"constraint:OnDelete:CASCADE;"`
}
type Shift struct {
Id uint `gorm:"primaryKey"`
Description string
ProductAmount uint
ShiftDate int64
WorkAreaId string // want "Foreign key should have type like int, not \"string\""
WorkArea WorkArea `gorm:"foreignKey:WorkAreaId;"`
}