Skip to content

Commit f136dfb

Browse files
authored
Go: add GoTemplate, GoPattern, MethodMatcher, TypeUtils, and rename module (#7235)
Adds comprehensive Go recipe infrastructure to rewrite-go: - **GoTemplate/GoPattern** (`pkg/template/`): Refaster-like template matching and replacement for Go. Supports captures, multiple before patterns (anyOf), scaffold parsing, structural comparison, and a `NewRecipe()` builder for declarative recipes. - **MethodMatcher** (`pkg/matcher/`): AspectJ-style pattern matching for Go method invocations (e.g., `"fmt Sprintf(..)"`) with glob wildcards and type-aware matching. - **TypeUtils** (`pkg/matcher/`): Type checking utilities — `IsAssignableTo()`, `IsOfClassType()`, `IsError()`, `IsString()`, `AsClass()`, `TypeOfExpression()`, `DeclaringTypeFQN()`. - **Markup helpers** (`pkg/tree/markers.go`): `MarkupWarn()`, `MarkupInfo()`, `MarkupError()` convenience functions for attaching severity-level diagnostics to LST nodes. - **GoVisitor completeness**: Fixed 7 visit methods to properly recurse into children (VariableDeclarations, VariableDeclarator, MethodInvocation, Return, If, Assignment, AssignmentOperation). - **Parser fix**: Fixed comma-in-whitespace bug in `mapReturnStmt` — commas between return expressions are now properly consumed and stored in `RightPadded.After`. - **Composite recipe support**: `test.RewriteRun` now handles `RecipeList()` sub-recipes. - **Module rename**: Changed module path from `github.com/openrewrite/rewrite` to `github.com/openrewrite/rewrite/rewrite-go` for proper Go submodule publishing within the monorepo.
1 parent 18e5bb7 commit f136dfb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+3591
-109
lines changed

rewrite-go/rewrite/cmd/rpc/main.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@ import (
3232

3333
"github.com/google/uuid"
3434

35-
goparser "github.com/openrewrite/rewrite/pkg/parser"
36-
"github.com/openrewrite/rewrite/pkg/printer"
37-
"github.com/openrewrite/rewrite/pkg/recipe"
38-
"github.com/openrewrite/rewrite/pkg/recipe/golang"
39-
"github.com/openrewrite/rewrite/pkg/recipe/installer"
40-
"github.com/openrewrite/rewrite/pkg/rpc"
41-
"github.com/openrewrite/rewrite/pkg/tree"
35+
goparser "github.com/openrewrite/rewrite/rewrite-go/pkg/parser"
36+
"github.com/openrewrite/rewrite/rewrite-go/pkg/printer"
37+
"github.com/openrewrite/rewrite/rewrite-go/pkg/recipe"
38+
"github.com/openrewrite/rewrite/rewrite-go/pkg/recipe/golang"
39+
"github.com/openrewrite/rewrite/rewrite-go/pkg/recipe/installer"
40+
"github.com/openrewrite/rewrite/rewrite-go/pkg/rpc"
41+
"github.com/openrewrite/rewrite/rewrite-go/pkg/tree"
4242
)
4343

4444
// jsonRPCRequest represents an incoming JSON-RPC 2.0 message (request or response).

rewrite-go/rewrite/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
module github.com/openrewrite/rewrite
1+
module github.com/openrewrite/rewrite/rewrite-go
22

33
go 1.23
44

Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Moderne Source Available License (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://docs.moderne.io/licensing/moderne-source-available-license
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package matcher
18+
19+
import (
20+
"testing"
21+
22+
"github.com/openrewrite/rewrite/rewrite-go/pkg/parser"
23+
"github.com/openrewrite/rewrite/rewrite-go/pkg/tree"
24+
"github.com/openrewrite/rewrite/rewrite-go/pkg/visitor"
25+
)
26+
27+
// --- TypeUtils tests ---
28+
29+
func TestGetFullyQualifiedName(t *testing.T) {
30+
tests := []struct {
31+
name string
32+
typ tree.JavaType
33+
want string
34+
}{
35+
{"nil", nil, ""},
36+
{"class", &tree.JavaTypeClass{FullyQualifiedName: "fmt.Stringer"}, "fmt.Stringer"},
37+
{"primitive", &tree.JavaTypePrimitive{Keyword: "int"}, "int"},
38+
{"parameterized", &tree.JavaTypeParameterized{Type: &tree.JavaTypeClass{FullyQualifiedName: "map"}}, "map"},
39+
{"unknown", &tree.JavaTypeUnknown{}, ""},
40+
}
41+
for _, tt := range tests {
42+
t.Run(tt.name, func(t *testing.T) {
43+
if got := GetFullyQualifiedName(tt.typ); got != tt.want {
44+
t.Errorf("GetFullyQualifiedName() = %q, want %q", got, tt.want)
45+
}
46+
})
47+
}
48+
}
49+
50+
func TestIsOfClassType(t *testing.T) {
51+
cls := &tree.JavaTypeClass{FullyQualifiedName: "time.Time"}
52+
if !IsOfClassType(cls, "time.Time") {
53+
t.Error("expected true for exact match")
54+
}
55+
if IsOfClassType(cls, "time.Duration") {
56+
t.Error("expected false for different type")
57+
}
58+
}
59+
60+
func TestIsAssignableTo(t *testing.T) {
61+
stringer := &tree.JavaTypeClass{FullyQualifiedName: "fmt.Stringer"}
62+
myType := &tree.JavaTypeClass{
63+
FullyQualifiedName: "main.MyType",
64+
Interfaces: []*tree.JavaTypeClass{stringer},
65+
}
66+
67+
if !IsAssignableTo(myType, "main.MyType") {
68+
t.Error("type should be assignable to itself")
69+
}
70+
if !IsAssignableTo(myType, "fmt.Stringer") {
71+
t.Error("type should be assignable to its interface")
72+
}
73+
if IsAssignableTo(myType, "io.Reader") {
74+
t.Error("type should not be assignable to unrelated interface")
75+
}
76+
}
77+
78+
func TestIsError(t *testing.T) {
79+
errType := &tree.JavaTypeClass{FullyQualifiedName: "error"}
80+
if !IsError(errType) {
81+
t.Error("expected true for error type")
82+
}
83+
if IsError(&tree.JavaTypeClass{FullyQualifiedName: "string"}) {
84+
t.Error("expected false for non-error type")
85+
}
86+
}
87+
88+
func TestIsString(t *testing.T) {
89+
if !IsString(&tree.JavaTypePrimitive{Keyword: "String"}) {
90+
t.Error("expected true for String primitive")
91+
}
92+
if IsString(&tree.JavaTypePrimitive{Keyword: "int"}) {
93+
t.Error("expected false for int")
94+
}
95+
}
96+
97+
func TestAsClass(t *testing.T) {
98+
cls := &tree.JavaTypeClass{FullyQualifiedName: "foo.Bar"}
99+
if AsClass(cls) != cls {
100+
t.Error("AsClass should return the class directly")
101+
}
102+
param := &tree.JavaTypeParameterized{Type: cls}
103+
if AsClass(param) != cls {
104+
t.Error("AsClass should unwrap parameterized types")
105+
}
106+
if AsClass(&tree.JavaTypePrimitive{Keyword: "int"}) != nil {
107+
t.Error("AsClass should return nil for non-class types")
108+
}
109+
}
110+
111+
// --- MethodMatcher tests ---
112+
113+
func TestGlobToRegexp(t *testing.T) {
114+
tests := []struct {
115+
pattern string
116+
input string
117+
want bool
118+
}{
119+
{"fmt", "fmt", true},
120+
{"fmt", "log", false},
121+
{"*", "fmt", true},
122+
{"*", "time.Time", false}, // * doesn't match dots
123+
{"*..*", "time.Time", true},
124+
{"*..*", "io", true},
125+
{"time.*", "time.Time", true},
126+
{"time.*", "time.Duration", true},
127+
{"time.*", "fmt.Stringer", false},
128+
{"Sprintf", "Sprintf", true},
129+
{"Sprint*", "Sprintf", true},
130+
{"Sprint*", "Sprint", true},
131+
{"Sprint*", "Println", false},
132+
}
133+
for _, tt := range tests {
134+
re := globToRegexp(tt.pattern)
135+
got := re.MatchString(tt.input)
136+
if got != tt.want {
137+
t.Errorf("globToRegexp(%q).MatchString(%q) = %v, want %v", tt.pattern, tt.input, got, tt.want)
138+
}
139+
}
140+
}
141+
142+
func TestMethodMatcherParsing(t *testing.T) {
143+
mm := NewMethodMatcher("fmt Sprintf(string, ..)")
144+
if mm.declaringTypePattern == nil {
145+
t.Fatal("expected declaringTypePattern")
146+
}
147+
if mm.methodNamePattern == nil {
148+
t.Fatal("expected methodNamePattern")
149+
}
150+
if !mm.matchesAnyArgs {
151+
t.Error("expected matchesAnyArgs for .. pattern")
152+
}
153+
}
154+
155+
func TestMethodMatcherMatchesAnyArgs(t *testing.T) {
156+
mm := NewMethodMatcher("fmt Println(..)")
157+
mi := &tree.MethodInvocation{
158+
Select: &tree.RightPadded[tree.Expression]{
159+
Element: &tree.Identifier{Name: "fmt"},
160+
},
161+
Name: &tree.Identifier{Name: "Println"},
162+
}
163+
if !mm.Matches(mi) {
164+
t.Error("expected match for fmt.Println with any args")
165+
}
166+
}
167+
168+
func TestMethodMatcherNoMatchWrongName(t *testing.T) {
169+
mm := NewMethodMatcher("fmt Println(..)")
170+
mi := &tree.MethodInvocation{
171+
Select: &tree.RightPadded[tree.Expression]{
172+
Element: &tree.Identifier{Name: "fmt"},
173+
},
174+
Name: &tree.Identifier{Name: "Sprintf"},
175+
}
176+
if mm.Matches(mi) {
177+
t.Error("expected no match for wrong method name")
178+
}
179+
}
180+
181+
func TestMethodMatcherNoMatchWrongPackage(t *testing.T) {
182+
mm := NewMethodMatcher("fmt Println(..)")
183+
mi := &tree.MethodInvocation{
184+
Select: &tree.RightPadded[tree.Expression]{
185+
Element: &tree.Identifier{Name: "log"},
186+
},
187+
Name: &tree.Identifier{Name: "Println"},
188+
}
189+
if mm.Matches(mi) {
190+
t.Error("expected no match for wrong package")
191+
}
192+
}
193+
194+
func TestMethodMatcherWildcardType(t *testing.T) {
195+
mm := NewMethodMatcher("* Sub(..)")
196+
mi := &tree.MethodInvocation{
197+
Select: &tree.RightPadded[tree.Expression]{
198+
Element: &tree.Identifier{Name: "t"},
199+
},
200+
Name: &tree.Identifier{Name: "Sub"},
201+
}
202+
// With just an identifier "t" as select, DeclaringTypeFQN returns "t"
203+
// which matches "*" pattern
204+
if !mm.Matches(mi) {
205+
t.Error("expected match for wildcard declaring type")
206+
}
207+
}
208+
209+
func TestMethodMatcherWithTypeInfo(t *testing.T) {
210+
mm := NewMethodMatcher("fmt Sprintf(..)")
211+
fmtType := &tree.JavaTypeClass{FullyQualifiedName: "fmt"}
212+
mi := &tree.MethodInvocation{
213+
Select: &tree.RightPadded[tree.Expression]{
214+
Element: &tree.Identifier{Name: "fmt"},
215+
},
216+
Name: &tree.Identifier{Name: "Sprintf"},
217+
MethodType: &tree.JavaTypeMethod{
218+
DeclaringType: fmtType,
219+
Name: "Sprintf",
220+
},
221+
}
222+
if !mm.Matches(mi) {
223+
t.Error("expected match with type info")
224+
}
225+
}
226+
227+
func TestMethodMatcherMatchesMethod(t *testing.T) {
228+
mm := NewMethodMatcher("time.Time Sub(..)")
229+
mt := &tree.JavaTypeMethod{
230+
DeclaringType: &tree.JavaTypeClass{FullyQualifiedName: "time.Time"},
231+
Name: "Sub",
232+
}
233+
if !mm.MatchesMethod(mt) {
234+
t.Error("expected match for time.Time Sub(..)")
235+
}
236+
}
237+
238+
// --- Integration test: MethodMatcher against parsed Go code ---
239+
240+
func TestMethodMatcherOnParsedCode(t *testing.T) {
241+
p := parser.NewGoParser()
242+
cu, err := p.Parse("test.go", `package main
243+
244+
import "fmt"
245+
246+
func main() {
247+
fmt.Println("hello")
248+
fmt.Sprintf("%d", 42)
249+
}
250+
`)
251+
if err != nil {
252+
t.Fatal(err)
253+
}
254+
255+
printlnMatcher := NewMethodMatcher("fmt Println(..)")
256+
sprintfMatcher := NewMethodMatcher("fmt Sprintf(..)")
257+
258+
var printlnCount, sprintfCount int
259+
v := visitor.Init(&methodMatcherVisitor{
260+
matchers: map[string]*MethodMatcher{
261+
"println": printlnMatcher,
262+
"sprintf": sprintfMatcher,
263+
},
264+
counts: map[string]*int{
265+
"println": &printlnCount,
266+
"sprintf": &sprintfCount,
267+
},
268+
})
269+
v.Visit(cu, nil)
270+
271+
if printlnCount != 1 {
272+
t.Errorf("expected 1 Println match, got %d", printlnCount)
273+
}
274+
if sprintfCount != 1 {
275+
t.Errorf("expected 1 Sprintf match, got %d", sprintfCount)
276+
}
277+
}
278+
279+
func TestMethodMatcherNoMatchOnParsedCode(t *testing.T) {
280+
p := parser.NewGoParser()
281+
cu, err := p.Parse("test.go", `package main
282+
283+
import "log"
284+
285+
func main() {
286+
log.Println("hello")
287+
}
288+
`)
289+
if err != nil {
290+
t.Fatal(err)
291+
}
292+
293+
fmtPrintln := NewMethodMatcher("fmt Println(..)")
294+
295+
var count int
296+
v := visitor.Init(&methodMatcherVisitor{
297+
matchers: map[string]*MethodMatcher{"match": fmtPrintln},
298+
counts: map[string]*int{"match": &count},
299+
})
300+
v.Visit(cu, nil)
301+
302+
if count != 0 {
303+
t.Errorf("expected 0 matches for fmt.Println in log code, got %d", count)
304+
}
305+
}
306+
307+
// Test helper visitor that counts method matcher hits.
308+
type methodMatcherVisitor struct {
309+
visitor.GoVisitor
310+
matchers map[string]*MethodMatcher
311+
counts map[string]*int
312+
}
313+
314+
func (v *methodMatcherVisitor) VisitMethodInvocation(mi *tree.MethodInvocation, p any) tree.J {
315+
mi = v.GoVisitor.VisitMethodInvocation(mi, p).(*tree.MethodInvocation)
316+
for name, matcher := range v.matchers {
317+
if matcher.Matches(mi) {
318+
*v.counts[name]++
319+
}
320+
}
321+
return mi
322+
}

0 commit comments

Comments
 (0)