feat: null safety
This commit is contained in:
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
9
.idea/gormlint.iml
generated
Normal file
9
.idea/gormlint.iml
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
10
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
10
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="GoDfaErrorMayBeNotNil" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<methods>
|
||||
<method importPath="github.com/fatih/structtag" receiver="*Tags" name="Get" />
|
||||
</methods>
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/gormlint.iml" filepath="$PROJECT_DIR$/.idea/gormlint.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
7
.idea/vcs.xml
generated
Normal file
7
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/cmd/gormlint" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
10
cmd/gormlint/main.go
Normal file
10
cmd/gormlint/main.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"golang.org/x/tools/go/analysis/singlechecker"
|
||||
"gormlint/nullSafetyCheck"
|
||||
)
|
||||
|
||||
func main() {
|
||||
singlechecker.Main(nullSafetyCheck.NullSafetyAnalyzer)
|
||||
}
|
||||
68
common/nullSafetyCheck.go
Normal file
68
common/nullSafetyCheck.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/fatih/structtag"
|
||||
"go/ast"
|
||||
"golang.org/x/tools/go/analysis"
|
||||
)
|
||||
|
||||
func isPointerType(typeExpr ast.Expr) bool {
|
||||
isPointer := false
|
||||
if _, ok := typeExpr.(*ast.StarExpr); ok {
|
||||
isPointer = true
|
||||
}
|
||||
return isPointer
|
||||
}
|
||||
|
||||
func isGormValueNullable(tags *structtag.Tags) (*bool, error) {
|
||||
gormTag, err := tags.Get("gorm")
|
||||
if gormTag == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
gormTag.Options = append([]string{gormTag.Name}, gormTag.Options...)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
nullTagExist := gormTag.HasOption("null")
|
||||
notNullTagExist := gormTag.HasOption("not null")
|
||||
|
||||
if nullTagExist == notNullTagExist && nullTagExist == true {
|
||||
return nil, errors.New(`tags "null" and "not null" are specified at one field`)
|
||||
}
|
||||
|
||||
if nullTagExist {
|
||||
return PointerOf(true), nil
|
||||
} else if notNullTagExist {
|
||||
return PointerOf(false), nil
|
||||
} else {
|
||||
return PointerOf(false), nil
|
||||
}
|
||||
}
|
||||
|
||||
func CheckFieldNullConsistency(pass analysis.Pass, field ast.Field, structName string, structTags string) {
|
||||
tags, err := structtag.Parse(structTags)
|
||||
if err != nil {
|
||||
pass.Reportf(field.Pos(), "Invalid structure tag: %s", err)
|
||||
}
|
||||
if tags == nil {
|
||||
return
|
||||
}
|
||||
|
||||
isFieldNullable := isPointerType(field.Type)
|
||||
isColumnNullable, err := isGormValueNullable(tags)
|
||||
|
||||
if err != nil {
|
||||
pass.Reportf(field.Pos(), "Null safety error: %s", err)
|
||||
}
|
||||
if isColumnNullable == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if isFieldNullable != *isColumnNullable {
|
||||
pass.Reportf(field.Pos(), "Null safety error in \"%s\" model, field \"%s\": column nullable policy doesn't match to tag nullable policy", structName, field.Names[0].Name)
|
||||
}
|
||||
}
|
||||
5
common/pointerOf.go
Normal file
5
common/pointerOf.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package common
|
||||
|
||||
func PointerOf[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
18
common/unnamedChecks.go
Normal file
18
common/unnamedChecks.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"golang.org/x/tools/go/analysis"
|
||||
)
|
||||
|
||||
func CheckUnnamedModel(pass analysis.Pass, typeSpec ast.TypeSpec) {
|
||||
if typeSpec.Name == nil {
|
||||
pass.Reportf(typeSpec.Pos(), "Unnamed model\n")
|
||||
}
|
||||
}
|
||||
|
||||
func CheckUnnamedField(pass analysis.Pass, structName string, field ast.Field) {
|
||||
if len(field.Names) == 0 {
|
||||
pass.Reportf(field.Pos(), "Struct \"%s\" has unnamed field", structName)
|
||||
}
|
||||
}
|
||||
15
go.mod
Normal file
15
go.mod
Normal file
@@ -0,0 +1,15 @@
|
||||
module gormlint
|
||||
|
||||
go 1.23.2
|
||||
|
||||
require golang.org/x/tools v0.28.0
|
||||
|
||||
require (
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fatih/structtag v1.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
)
|
||||
21
go.sum
Normal file
21
go.sum
Normal file
@@ -0,0 +1,21 @@
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
|
||||
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
|
||||
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
|
||||
26
models/testdata.go
Normal file
26
models/testdata.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package models
|
||||
|
||||
type Order struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
Status string
|
||||
ProductTypeId uint
|
||||
ProductType ProductType
|
||||
ProductAmount uint
|
||||
Description string
|
||||
CustomerId uint `gorm:"null;foreignKey:CustomerId;"`
|
||||
Customer Customer
|
||||
CreatedAt int64 `gorm:"autoCreateTime"`
|
||||
DeadlineDate int64
|
||||
}
|
||||
|
||||
type ProductType struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
Name string
|
||||
}
|
||||
|
||||
type Customer struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
Title string
|
||||
Contact string
|
||||
Orders []Order `gorm:"foreignKey:CustomerId"`
|
||||
}
|
||||
44
nullSafetyCheck/nullSafetyCheck.go
Normal file
44
nullSafetyCheck/nullSafetyCheck.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package nullSafetyCheck
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"golang.org/x/tools/go/analysis"
|
||||
"gormlint/common"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var NullSafetyAnalyzer = &analysis.Analyzer{
|
||||
Name: "nullSafety",
|
||||
Doc: "reports inconsistency of nullable values",
|
||||
Run: run,
|
||||
}
|
||||
|
||||
func run(pass *analysis.Pass) (any, error) {
|
||||
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
|
||||
}
|
||||
|
||||
common.CheckUnnamedModel(*pass, *typeSpec)
|
||||
|
||||
for _, field := range structure.Fields.List {
|
||||
common.CheckUnnamedField(*pass, typeSpec.Name.Name, *field)
|
||||
if field.Tag != nil {
|
||||
tagWithoutQuotes := field.Tag.Value[1 : len(field.Tag.Value)-1]
|
||||
tagWithoutSemicolons := strings.ReplaceAll(tagWithoutQuotes, ";", ",")
|
||||
common.CheckFieldNullConsistency(*pass, *field, typeSpec.Name.Name, tagWithoutSemicolons)
|
||||
} else {
|
||||
// TODO: check necessary tags for some fields
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
Reference in New Issue
Block a user