feat: foreignKeys check
+fix: "unnamed" field bug
This commit is contained in:
@@ -32,12 +32,18 @@ func ParseModels(pass *analysis.Pass, models *map[string]Model) {
|
||||
|
||||
for _, field := range structure.Fields.List {
|
||||
var structField Field
|
||||
if err := CheckUnnamedField(typeSpec.Name.Name, *field); err != nil {
|
||||
pass.Reportf(field.Pos(), err.Error())
|
||||
return false
|
||||
|
||||
if len(field.Names) == 0 {
|
||||
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.Comment = field.Comment.Text()
|
||||
structField.Type = field.Type
|
||||
|
||||
@@ -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)
|
||||
if err != nil {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -12,10 +12,3 @@ func CheckUnnamedModel(typeSpec ast.TypeSpec) error {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1 +1,49 @@
|
||||
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
|
||||
}
|
||||
|
||||
@@ -32,20 +32,26 @@ func run(pass *analysis.Pass) (any, error) {
|
||||
}
|
||||
|
||||
for _, field := range structure.Fields.List {
|
||||
if err := common.CheckUnnamedField(typeSpec.Name.Name, *field); err != nil {
|
||||
pass.Reportf(field.Pos(), err.Error())
|
||||
return false
|
||||
var structFieldName string
|
||||
if len(field.Names) == 0 {
|
||||
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 {
|
||||
tagWithoutQuotes := field.Tag.Value[1 : len(field.Tag.Value)-1]
|
||||
tagWithoutSemicolons := strings.ReplaceAll(tagWithoutQuotes, ";", ",")
|
||||
err := common.CheckFieldNullConsistency(*field, typeSpec.Name.Name, tagWithoutSemicolons)
|
||||
err := common.CheckFieldNullConsistency(*field, structFieldName, typeSpec.Name.Name, tagWithoutSemicolons)
|
||||
if err != nil {
|
||||
pass.Reportf(field.Pos(), err.Error())
|
||||
return false
|
||||
}
|
||||
}
|
||||
// TODO: check necessary tags for some fields
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
@@ -13,10 +13,8 @@ var ReferenceAnalyzer = &analysis.Analyzer{
|
||||
Run: run,
|
||||
}
|
||||
|
||||
var models map[string]common.Model
|
||||
|
||||
func run(pass *analysis.Pass) (any, error) {
|
||||
models = make(map[string]common.Model)
|
||||
models := make(map[string]common.Model)
|
||||
common.ParseModels(pass, &models)
|
||||
|
||||
for _, model := range models {
|
||||
|
||||
12
tests/foreign_key_check_test.go
Normal file
12
tests/foreign_key_check_test.go
Normal 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")
|
||||
}
|
||||
12
tests/testdata/src/foreign_key_check/negative.go
vendored
Normal file
12
tests/testdata/src/foreign_key_check/negative.go
vendored
Normal 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
|
||||
}
|
||||
27
tests/testdata/src/foreign_key_check/positive.go
vendored
Normal file
27
tests/testdata/src/foreign_key_check/positive.go
vendored
Normal 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;"`
|
||||
}
|
||||
Reference in New Issue
Block a user