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