From ba4ace6f4b2002dffbd07de49f5483b8b9ca5f85 Mon Sep 17 00:00:00 2001 From: raeperd Date: Sat, 4 Apr 2026 00:07:08 +0900 Subject: [PATCH 1/2] feat: add SuggestedFixes for --fix support Attach SuggestedFixes to diagnostics so golangci-lint --fix can automatically convert value receivers to pointer receivers. Switch tests to RunWithSuggestedFixes and add golden files. --- analyzer.go | 24 ++++++++++++-- analyzer_test.go | 2 +- testdata/src/basic/rpc.go.golden | 33 ++++++++++++++++++++ testdata/src/disablebuiltin/binary.go.golden | 11 +++++++ testdata/src/disablebuiltin/gob.go.golden | 11 +++++++ testdata/src/disablebuiltin/json.go.golden | 11 +++++++ testdata/src/disablebuiltin/text.go.golden | 11 +++++++ testdata/src/disablebuiltin/xml.go.golden | 13 ++++++++ testdata/src/disablebuiltin/yaml.go.golden | 13 ++++++++ 9 files changed, 125 insertions(+), 4 deletions(-) create mode 100644 testdata/src/basic/rpc.go.golden create mode 100644 testdata/src/disablebuiltin/binary.go.golden create mode 100644 testdata/src/disablebuiltin/gob.go.golden create mode 100644 testdata/src/disablebuiltin/json.go.golden create mode 100644 testdata/src/disablebuiltin/text.go.golden create mode 100644 testdata/src/disablebuiltin/xml.go.golden create mode 100644 testdata/src/disablebuiltin/yaml.go.golden diff --git a/analyzer.go b/analyzer.go index a67d72e..30f339d 100644 --- a/analyzer.go +++ b/analyzer.go @@ -1,6 +1,7 @@ package recvcheck import ( + "fmt" "go/ast" "golang.org/x/tools/go/analysis" @@ -88,12 +89,28 @@ func (r *analyzer) run(pass *analysis.Pass) (any, error) { st.starUsed = true } else { st.typeUsed = true + st.valueRecvTypes = append(st.valueRecvTypes, 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) + edits := make([]analysis.TextEdit, len(st.valueRecvTypes)) + for i, ident := range st.valueRecvTypes { + edits[i] = analysis.TextEdit{ + Pos: ident.Pos(), + End: ident.Pos(), + NewText: []byte("*"), + } + } + pass.Report(analysis.Diagnostic{ + Pos: pass.Pkg.Scope().Lookup(recv).Pos(), + Message: fmt.Sprintf("the methods of %q use pointer receiver and non-pointer receiver.", recv), + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf("use pointer receiver for %q", recv), + TextEdits: edits, + }}, + }) } } @@ -116,8 +133,9 @@ func (r *analyzer) isExcluded(recv *ast.Ident, f *ast.FuncDecl) bool { } type structType struct { - starUsed bool - typeUsed bool + starUsed bool + typeUsed bool + valueRecvTypes []*ast.Ident } func recvTypeIdent(r ast.Expr) (*ast.Ident, bool) { diff --git a/analyzer_test.go b/analyzer_test.go index 5a34251..e8b2b11 100644 --- a/analyzer_test.go +++ b/analyzer_test.go @@ -38,7 +38,7 @@ func TestAnalyzer(t *testing.T) { t.Run(test.desc, func(t *testing.T) { a := recvcheck.NewAnalyzer(test.settings) - analysistest.Run(t, analysistest.TestData(), a, test.desc) + analysistest.RunWithSuggestedFixes(t, analysistest.TestData(), a, test.desc) }) } } diff --git a/testdata/src/basic/rpc.go.golden b/testdata/src/basic/rpc.go.golden new file mode 100644 index 0000000..cf71c70 --- /dev/null +++ b/testdata/src/basic/rpc.go.golden @@ -0,0 +1,33 @@ +package basic + +import ( + "time" +) + +type RPC struct { // want `the methods of "RPC" use pointer receiver and non-pointer receiver.` + result int + done chan struct{} +} + +func (rpc *RPC) compute() { + time.Sleep(time.Second) // strenuous computation intensifies + rpc.result = 42 + close(rpc.done) +} + +func (*RPC) version() int { + return 1 // never going to need to change this +} + +// Following main function cause data race error +// reference: https://dave.cheney.net/2015/11/18/wednesday-pop-quiz-spot-the-race +// func main() { +// rpc := &RPC{done: make(chan struct{})} +// +// go rpc.compute() // kick off computation in the background +// version := rpc.version() // grab some other information while we're waiting +// <-rpc.done // wait for computation to finish +// result := rpc.result +// +// fmt.Printf("RPC computation complete, result: %d, version: %d\n", result, version) +// } diff --git a/testdata/src/disablebuiltin/binary.go.golden b/testdata/src/disablebuiltin/binary.go.golden new file mode 100644 index 0000000..49d54ca --- /dev/null +++ b/testdata/src/disablebuiltin/binary.go.golden @@ -0,0 +1,11 @@ +package disablebuiltin + +type Binary struct{} // want `the methods of "Binary" use pointer receiver and non-pointer receiver.` + +func (b *Binary) MarshalBinary() ([]byte, error) { + panic("not implemented") +} + +func (b *Binary) UnmarshalBinary(data []byte) error { + panic("not implemented") +} diff --git a/testdata/src/disablebuiltin/gob.go.golden b/testdata/src/disablebuiltin/gob.go.golden new file mode 100644 index 0000000..29b5b5a --- /dev/null +++ b/testdata/src/disablebuiltin/gob.go.golden @@ -0,0 +1,11 @@ +package disablebuiltin + +type Gob struct{} // want `the methods of "Gob" use pointer receiver and non-pointer receiver.` + +func (g *Gob) GobEncode() ([]byte, error) { + panic("not implemented") +} + +func (g *Gob) GobDecode(data []byte) error { + panic("not implemented") +} diff --git a/testdata/src/disablebuiltin/json.go.golden b/testdata/src/disablebuiltin/json.go.golden new file mode 100644 index 0000000..0cbeca9 --- /dev/null +++ b/testdata/src/disablebuiltin/json.go.golden @@ -0,0 +1,11 @@ +package disablebuiltin + +type JSON struct{} // want `the methods of "JSON" use pointer receiver and non-pointer receiver.` + +func (j *JSON) MarshalJSON() ([]byte, error) { + panic("not implemented") +} + +func (j *JSON) UnmarshalJSON(b []byte) error { + panic("not implemented") +} diff --git a/testdata/src/disablebuiltin/text.go.golden b/testdata/src/disablebuiltin/text.go.golden new file mode 100644 index 0000000..d9ccb21 --- /dev/null +++ b/testdata/src/disablebuiltin/text.go.golden @@ -0,0 +1,11 @@ +package disablebuiltin + +type Text struct{} // want `the methods of "Text" use pointer receiver and non-pointer receiver.` + +func (t *Text) MarshalText() ([]byte, error) { + panic("not implemented") +} + +func (t *Text) UnmarshalText(b []byte) error { + panic("not implemented") +} diff --git a/testdata/src/disablebuiltin/xml.go.golden b/testdata/src/disablebuiltin/xml.go.golden new file mode 100644 index 0000000..cc063d8 --- /dev/null +++ b/testdata/src/disablebuiltin/xml.go.golden @@ -0,0 +1,13 @@ +package disablebuiltin + +import "encoding/xml" + +type XML struct{} // want `the methods of "XML" use pointer receiver and non-pointer receiver.` + +func (x *XML) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + panic("not implemented") +} + +func (x *XML) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + panic("not implemented") +} diff --git a/testdata/src/disablebuiltin/yaml.go.golden b/testdata/src/disablebuiltin/yaml.go.golden new file mode 100644 index 0000000..456d819 --- /dev/null +++ b/testdata/src/disablebuiltin/yaml.go.golden @@ -0,0 +1,13 @@ +package disablebuiltin + +type Node struct{} + +type YAML struct{} // want `the methods of "YAML" use pointer receiver and non-pointer receiver.` + +func (j *YAML) MarshalYAML() (any, error) { + panic("not implemented") +} + +func (j *YAML) UnmarshalYAML(value *Node) error { + panic("not implemented") +} From c0ef36c09d5ac0ace2970b1a244e63e70b91f7fa Mon Sep 17 00:00:00 2001 From: raeperd Date: Sat, 4 Apr 2026 00:07:38 +0900 Subject: [PATCH 2/2] refactor: replace typeUsed with len(valueRecvTypes) > 0 --- analyzer.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/analyzer.go b/analyzer.go index 30f339d..b442193 100644 --- a/analyzer.go +++ b/analyzer.go @@ -88,13 +88,12 @@ func (r *analyzer) run(pass *analysis.Pass) (any, error) { if isStar { st.starUsed = true } else { - st.typeUsed = true st.valueRecvTypes = append(st.valueRecvTypes, recv) } }) for recv, st := range structs { - if st.starUsed && st.typeUsed { + if st.starUsed && len(st.valueRecvTypes) > 0 { edits := make([]analysis.TextEdit, len(st.valueRecvTypes)) for i, ident := range st.valueRecvTypes { edits[i] = analysis.TextEdit{ @@ -134,7 +133,6 @@ func (r *analyzer) isExcluded(recv *ast.Ident, f *ast.FuncDecl) bool { type structType struct { starUsed bool - typeUsed bool valueRecvTypes []*ast.Ident }