feat: references check
... and fixes: parse fields without tags and other logical errors
This commit is contained in:
@@ -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
77
common/modelsParser.go
Normal 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
21
common/resolveBaseType.go
Normal 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
|
||||||
|
}
|
||||||
1
foreignKeyCheck/foreignKeyCheck.go
Normal file
1
foreignKeyCheck/foreignKeyCheck.go
Normal file
@@ -0,0 +1 @@
|
|||||||
|
package foreignKeyCheck
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
12
tests/reference_check_test.go
Normal file
12
tests/reference_check_test.go
Normal 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")
|
||||||
|
}
|
||||||
24
tests/testdata/src/references_check/negative.go
vendored
Normal file
24
tests/testdata/src/references_check/negative.go
vendored
Normal 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;"`
|
||||||
|
}
|
||||||
19
tests/testdata/src/references_check/positive.go
vendored
Normal file
19
tests/testdata/src/references_check/positive.go
vendored
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user