Skip to content

Commit

Permalink
feat: Implements NewAnalyzer with Setting struct
Browse files Browse the repository at this point in the history
  • Loading branch information
raeperd committed Nov 15, 2024
1 parent 232da96 commit 666cca7
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 61 deletions.
157 changes: 98 additions & 59 deletions analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,84 +10,123 @@ import (
"golang.org/x/tools/go/ast/inspector"
)

var Analyzer = &analysis.Analyzer{
Name: "recvcheck",
Doc: "checks for receiver type consistency",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer},
// NewAnalyzer returns a new analyzer to check for receiver type consistency.
func NewAnalyzer(s Setting) *analysis.Analyzer {
excludeMethods := []string{ // Default excludes for Marshal/Encode/Value methods #7
"MarshalText() ([]byte, error)",
"MarshalJSON() ([]byte, error)",
"MarshalXML(*xml.Encoder, xml.StartElement) error",
"MarshalBinary() ([]byte, error)",
"GobEncode() ([]byte, error)",
"Value() (driver.Value, error)",
"Value() (any, error)",
"Value() (interface{}, error)",
}
if s.NoBuiltinExcludeMethod {
excludeMethods = []string{}
}
excludeMethods = append(excludeMethods, s.ExcludeMethod...)

filter := newMethodSignatureFilter(excludeMethods...)

return &analysis.Analyzer{
Name: "recvcheck",
Doc: "checks for receiver type consistency",
Run: runWithFilter(filter),
Requires: []*analysis.Analyzer{inspect.Analyzer},
}
}

func run(pass *analysis.Pass) (any, error) {
inspector := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
methodSignatureFilter := newMethodSignatureFilter(false)
// Setting is the configuration for the analyzer.
type Setting struct {
// ExcludeMethod specifies method signatures to exclude from receiver type checking.
// Each signature should be in the format "MethodName(paramTypes) returnTypes".
//
// Examples of valid signatures:
// - "MarshalJSON() ([]byte, error)"
// - "UnmarshalJSON([]byte) error"
// - "String() string"
//
// These signatures are merged with built-in excluded signatures unless
// NoBuiltinExcludeMethod is set to true.
//
// Built-in excluded signatures:
// - "MarshalText() ([]byte, error)"
// - "MarshalJSON() ([]byte, error)"
// - "MarshalXML(*xml.Encoder, xml.StartElement) error"
// - "MarshalBinary() ([]byte, error)"
// - "GobEncode() ([]byte, error)"
// - "Value() (driver.Value, error)"
// - "Value() (any, error)"
// - "Value() (interface{}, error)"
ExcludeMethod []string
NoBuiltinExcludeMethod bool // if true, disables the built-in excluded method signatures.
}

structs := map[string]*structType{}
inspector.Preorder([]ast.Node{(*ast.FuncDecl)(nil)}, func(n ast.Node) {
funcDecl, ok := n.(*ast.FuncDecl)
if !ok || funcDecl.Recv == nil || len(funcDecl.Recv.List) != 1 {
return
}
if methodSignatureFilter(funcDecl) {
return
}
func runWithFilter(filter func(*ast.FuncDecl) bool) func(*analysis.Pass) (any, error) {
return func(pass *analysis.Pass) (interface{}, error) {
inspector := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

var recv *ast.Ident
var isStar bool
switch recvType := funcDecl.Recv.List[0].Type.(type) {
case *ast.StarExpr:
isStar = true
if recv, ok = recvType.X.(*ast.Ident); !ok {
structs := map[string]*structType{}
inspector.Preorder([]ast.Node{(*ast.FuncDecl)(nil)}, func(n ast.Node) {
funcDecl, ok := n.(*ast.FuncDecl)
if !ok || funcDecl.Recv == nil || len(funcDecl.Recv.List) != 1 {
return
}
if filter(funcDecl) {
return
}
case *ast.Ident:
recv = recvType
default:
return
}

st, ok := structs[recv.Name]
if !ok {
structs[recv.Name] = &structType{}
st = structs[recv.Name]
}
var recv *ast.Ident
var isStar bool
switch recvType := funcDecl.Recv.List[0].Type.(type) {
case *ast.StarExpr:
isStar = true
if recv, ok = recvType.X.(*ast.Ident); !ok {
return
}
case *ast.Ident:
recv = recvType
default:
return
}

if isStar {
st.starUsed = true
} else {
st.typeUsed = true
}
})
st, ok := structs[recv.Name]
if !ok {
structs[recv.Name] = &structType{}
st = structs[recv.Name]
}

if isStar {
st.starUsed = true
} else {
st.typeUsed = true
}
})

for recv, st := range structs {
if st.starUsed && st.typeUsed {
pass.Reportf(pass.Pkg.Scope().Lookup(recv).Pos(), "the methods of %q use pointer receiver and non-pointer receiver.", recv)
for recv, st := range structs {
if st.starUsed && st.typeUsed {
pass.Reportf(pass.Pkg.Scope().Lookup(recv).Pos(), "the methods of %q use pointer receiver and non-pointer receiver.", recv)
}
}
}

return nil, nil
return nil, nil
}
}

type structType struct {
starUsed bool
typeUsed bool
}

func newMethodSignatureFilter(noBuiltin bool, signatures ...string) func(*ast.FuncDecl) bool {
filter := map[string]struct{}{
"MarshalText() ([]byte, error)": {},
"MarshalJSON() ([]byte, error)": {},
"MarshalXML(*xml.Encoder, xml.StartElement) error": {},
"MarshalBinary() ([]byte, error)": {},
"GobEncode() ([]byte, error)": {},
"Value() (driver.Value, error)": {},
"Value() (any, error)": {},
"Value() (interface{}, error)": {},
}
if noBuiltin {
filter = map[string]struct{}{}
func newMethodSignatureFilter(signatures ...string) func(*ast.FuncDecl) bool {
if len(signatures) == 0 {
return func(*ast.FuncDecl) bool { return false }
}

excludes := make(map[string]struct{}, len(signatures))
for _, sig := range signatures {
filter[sig] = struct{}{}
excludes[sig] = struct{}{}
}

return func(f *ast.FuncDecl) bool {
Expand Down Expand Up @@ -121,7 +160,7 @@ func newMethodSignatureFilter(noBuiltin bool, signatures ...string) func(*ast.Fu
}
}

_, exists := filter[sig.String()]
_, exists := excludes[sig.String()]
return exists
}
}
2 changes: 1 addition & 1 deletion analyzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ import (
)

func TestAnalyzer(t *testing.T) {
analysistest.Run(t, analysistest.TestData(), recvcheck.Analyzer, "test")
analysistest.Run(t, analysistest.TestData(), recvcheck.NewAnalyzer(recvcheck.Setting{}), "test")
}
19 changes: 18 additions & 1 deletion cmd/recvcheck/main.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
package main

import (
"flag"
"strings"

"github.com/raeperd/recvcheck"
"golang.org/x/tools/go/analysis/singlechecker"
)

func main() {
singlechecker.Main(recvcheck.Analyzer)
var (
excludeMethod string
noBuiltinExcludeMethod bool
)
flag.StringVar(&excludeMethod, "exclude-method", "",
"exclude the method signature from the check, seperated by '/'")
flag.BoolVar(&noBuiltinExcludeMethod, "no-builtin-exclude-method", false,
`disables the default exclude methods such as "MarshalText() ([]byte, error)"`)
flag.Parse()

setting := recvcheck.Setting{
NoBuiltinExcludeMethod: noBuiltinExcludeMethod,
ExcludeMethod: strings.Split(excludeMethod, "/"),
}
singlechecker.Main(recvcheck.NewAnalyzer(setting))
}

0 comments on commit 666cca7

Please sign in to comment.