feat: references check

... and fixes: parse fields without tags and other logical errors
This commit is contained in:
2024-12-30 21:15:15 +07:00
parent d4ef9b3ec6
commit 49878c333c
8 changed files with 196 additions and 76 deletions

View File

@@ -11,13 +11,13 @@ type Field struct {
Tags *string Tags *string
Options []string // contains options like "autoCreateTime" or "null" Options []string // contains options like "autoCreateTime" or "null"
Params []string // contains params like "foreignKey:CustomerId" or "constrain:OnDelete:Cascade" Params []string // contains params like "foreignKey:CustomerId" or "constrain:OnDelete:Cascade"
Position token.Pos Pos token.Pos
Comment string Comment string
} }
type Model struct { type Model struct {
Name string Name string
Fields map[string]Field Fields map[string]Field
Position token.Pos Pos token.Pos
Comment string Comment string
} }

77
common/modelsParser.go Normal file
View File

@@ -0,0 +1,77 @@
package common
import (
"github.com/fatih/structtag"
"go/ast"
"golang.org/x/tools/go/analysis"
"strings"
)
func ParseModels(pass *analysis.Pass, models *map[string]Model) {
for _, file := range pass.Files {
ast.Inspect(file, func(node ast.Node) bool {
typeSpec, ok := node.(*ast.TypeSpec)
if !ok {
return true
}
structure, ok := typeSpec.Type.(*ast.StructType)
if !ok {
return true
}
if err := CheckUnnamedModel(*typeSpec); err != nil {
pass.Reportf(structure.Pos(), err.Error())
return false
}
var model Model
model.Name = typeSpec.Name.Name
model.Comment = typeSpec.Comment.Text()
model.Pos = structure.Pos()
model.Fields = make(map[string]Field)
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
}
structField.Name = field.Names[0].Name
structField.Pos = field.Pos()
structField.Comment = field.Comment.Text()
structField.Type = field.Type
if field.Tag != nil {
structField.Tags = &field.Tag.Value
tags, err := structtag.Parse(NormalizeStructTags(field.Tag.Value))
if err != nil {
pass.Reportf(field.Pos(), "Invalid structure tag: %s\n", err)
return false
}
if tags != nil {
gormTag, parseErr := tags.Get("gorm")
if gormTag != nil && parseErr == nil {
gormTag.Options = append([]string{gormTag.Name}, gormTag.Options...)
for _, opt := range gormTag.Options {
if strings.Contains(opt, ":") {
structField.Params = append(structField.Params, opt)
} else {
structField.Options = append(structField.Options, opt)
}
}
}
if parseErr != nil {
pass.Reportf(field.Pos(), "Invalid structure tag: %s\n", parseErr)
return false
}
}
}
model.Fields[structField.Name] = structField
(*models)[model.Name] = model
}
return false
})
}
}

21
common/resolveBaseType.go Normal file
View File

@@ -0,0 +1,21 @@
package common
import (
"go/ast"
)
func ResolveBaseType(expr ast.Expr) *string {
switch e := expr.(type) {
case *ast.Ident:
return &e.Name
case *ast.StarExpr:
return ResolveBaseType(e.X)
case *ast.ArrayType:
return ResolveBaseType(e.Elt)
case *ast.SelectorExpr:
return ResolveBaseType(e.X)
case *ast.ParenExpr:
return ResolveBaseType(e.X)
}
return nil
}

View File

@@ -0,0 +1 @@
package foreignKeyCheck

View File

@@ -1,8 +1,7 @@
package referencesCheck package referencesCheck
import ( import (
"github.com/fatih/structtag" "fmt"
"go/ast"
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"gormlint/common" "gormlint/common"
"strings" "strings"
@@ -17,77 +16,44 @@ var ReferenceAnalyzer = &analysis.Analyzer{
var models map[string]common.Model var models map[string]common.Model
func init() {
models = make(map[string]common.Model)
}
func run(pass *analysis.Pass) (any, error) { func run(pass *analysis.Pass) (any, error) {
// TODO: move in new function models = make(map[string]common.Model)
for _, file := range pass.Files { common.ParseModels(pass, &models)
ast.Inspect(file, func(node ast.Node) bool {
typeSpec, ok := node.(*ast.TypeSpec) for _, model := range models {
if !ok { for _, field := range model.Fields {
return true for _, param := range field.Params {
pair := strings.Split(param, ":")
if len(pair) < 2 {
fmt.Printf("%s", param)
} }
structure, ok := typeSpec.Type.(*ast.StructType) paramName := pair[0]
if !ok { paramValue := pair[1]
return true
if paramName == "reference" {
pass.Reportf(field.Pos, "Typo in tag: \"reference\" instead of verb \"references\"")
}
if paramName == "references" {
fieldType := common.ResolveBaseType(field.Type)
if fieldType == nil {
pass.Reportf(field.Pos, "Failed to process references check. Cannot resolve type \"%s\" in field \"%s\"", field.Type, field.Name)
return nil, nil
} }
if err := common.CheckUnnamedModel(*typeSpec); err != nil { relatedModel, modelExists := models[*fieldType]
pass.Reportf(structure.Pos(), err.Error())
return false
}
var model common.Model if modelExists {
model.Name = typeSpec.Name.Name _, fieldExists := relatedModel.Fields[paramValue]
model.Comment = typeSpec.Comment.Text() if !fieldExists {
model.Position = structure.Pos() pass.Reportf(field.Pos, "Related field \"%s\" doesn't exist on model \"%s\"", paramValue, relatedModel.Name)
model.Fields = make(map[string]common.Field)
for _, field := range structure.Fields.List {
var structField common.Field
if err := common.CheckUnnamedField(typeSpec.Name.Name, *field); err != nil {
pass.Reportf(field.Pos(), err.Error())
return false
} }
structField.Name = field.Names[0].Name
structField.Position = field.Pos()
structField.Comment = field.Comment.Text()
structField.Type = field.Type
if field.Tag != nil {
structField.Tags = &field.Tag.Value
tags, err := structtag.Parse(common.NormalizeStructTags(field.Tag.Value))
if err != nil {
pass.Reportf(field.Pos(), "Invalid structure tag: %s\n", err)
return false
}
if tags != nil {
gormTag, parseErr := tags.Get("gorm")
if gormTag != nil && parseErr == nil {
gormTag.Options = append([]string{gormTag.Name}, gormTag.Options...)
for _, opt := range gormTag.Options {
if strings.Contains(opt, ":") {
structField.Params = append(structField.Options, opt)
} else { } else {
structField.Options = append(structField.Options, opt) pass.Reportf(field.Pos, "Related model \"%s\" doesn't exist", *fieldType)
} }
} }
} }
if parseErr != nil {
pass.Reportf(field.Pos(), "Invalid structure tag: %s\n", parseErr)
return false
} }
} }
model.Fields[structField.Name] = structField
}
models[model.Name] = model
}
return false
})
}
return nil, nil return nil, nil
} }

View File

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

View File

@@ -0,0 +1,24 @@
package references_check
type WorkArea struct {
Id uint `gorm:"primaryKey"`
Workshop Workshop `gorm:"foreignKey:WorkshopId;references:Id;"`
WorkshopId uint
}
type Workshop struct {
Id uint `gorm:"primaryKey"`
Name string
WorkAreas []WorkArea `gorm:"constraint:OnDelete:CASCADE;"`
}
type TeamType struct {
Code uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
}
type TeamTask struct {
Id uint `gorm:"primaryKey"`
TeamTypeId uint
TeamType TeamType `gorm:"references:Code;"`
}

View File

@@ -0,0 +1,19 @@
package references_check
type User struct {
Name string
CompanyID string
Company Company `gorm:"references:code"` // want "Related field \"code\" doesn't exist on model \"Company\""
}
type Company struct {
ID int
Code string
Name string
}
type Order struct {
Id uint `gorm:"primaryKey"`
CompanyID string `gorm:"references:Code"` // want "Related model \"string\" doesn't exist"
Company Company
}