diff --git a/rewrite-go/.gitignore b/rewrite-go/.gitignore index cc77b22f021..f50e74755d3 100644 --- a/rewrite-go/.gitignore +++ b/rewrite-go/.gitignore @@ -1 +1 @@ -rewrite/test-classpath.txt +test-classpath.txt diff --git a/rewrite-go/pkg/parser/go_parser.go b/rewrite-go/pkg/parser/go_parser.go index 559780682f0..98dca336933 100644 --- a/rewrite-go/pkg/parser/go_parser.go +++ b/rewrite-go/pkg/parser/go_parser.go @@ -760,7 +760,7 @@ func (ctx *parseContext) mapFieldListAsParams(fl *ast.FieldList) tree.Container[ } else { closeParen := ctx.prefix(fl.Closing) ctx.skip(1) // ")" - if len(closeParen.Comments) > 0 { + if !closeParen.IsEmpty() { elements = append(elements, tree.RightPadded[tree.Statement]{ Element: &tree.Empty{ID: uuid.New()}, After: closeParen, @@ -2193,8 +2193,14 @@ func (ctx *parseContext) mapSliceExpr(expr *ast.SliceExpr) tree.Expression { // mapMapType maps a map type expression like `map[K]V`. func (ctx *parseContext) mapMapType(expr *ast.MapType) tree.Expression { prefix := ctx.prefixAndSkip(expr.Map, len("map")) - lbrackPrefix := ctx.prefix(expr.Map + token.Pos(len("map"))) - ctx.skip(1) // "[" + lbrackOff := ctx.findNext('[') + var lbrackPrefix tree.Space + if lbrackOff >= 0 { + lbrackPrefix = ctx.prefix(ctx.file.Pos(lbrackOff)) + ctx.skip(1) // "[" + } else { + ctx.skip(1) // "[" + } key := ctx.mapTypeExpr(expr.Key) rbrackOff := ctx.findNext(']') var rbrackPrefix tree.Space @@ -2216,15 +2222,40 @@ func (ctx *parseContext) mapMapType(expr *ast.MapType) tree.Expression { // mapChanType maps a channel type expression. func (ctx *parseContext) mapChanType(expr *ast.ChanType) tree.Expression { prefix := ctx.prefix(expr.Begin) + var markers tree.Markers var dir tree.ChanDir switch expr.Dir { case ast.SEND: dir = tree.ChanSendOnly - ctx.skip(len("chan<-")) + ctx.skip(len("chan")) + // Capture space before the arrow + arrowOff := ctx.findNext('<') + var dirMarkerBefore tree.Space + if arrowOff >= 0 { + dirMarkerBefore = ctx.prefix(ctx.file.Pos(arrowOff)) + ctx.cursor = arrowOff + } + ctx.skip(2) // "<-" + // Create marker to store the space + if !dirMarkerBefore.IsEmpty() { + markers = tree.Markers{ + ID: uuid.New(), + Entries: []tree.Marker{tree.ChanDirMarker{ + Ident: uuid.New(), + Before: dirMarkerBefore, + }}, + } + } case ast.RECV: dir = tree.ChanRecvOnly - ctx.skip(len("<-chan")) + ctx.skip(2) // "<-" + // Skip any whitespace/comments before "chan" + chanOff := ctx.findNext('c') + if chanOff >= 0 && chanOff+4 <= len(ctx.src) && string(ctx.src[chanOff:chanOff+4]) == "chan" { + ctx.cursor = chanOff + } + ctx.skip(len("chan")) default: dir = tree.ChanBidi ctx.skip(len("chan")) @@ -2232,10 +2263,11 @@ func (ctx *parseContext) mapChanType(expr *ast.ChanType) tree.Expression { value := ctx.mapTypeExpr(expr.Value) return &tree.Channel{ - ID: uuid.New(), - Prefix: prefix, - Dir: dir, - Value: value, + ID: uuid.New(), + Prefix: prefix, + Markers: markers, + Dir: dir, + Value: value, } } diff --git a/rewrite-go/pkg/printer/go_printer.go b/rewrite-go/pkg/printer/go_printer.go index 28e4dd20871..444f964ee77 100644 --- a/rewrite-go/pkg/printer/go_printer.go +++ b/rewrite-go/pkg/printer/go_printer.go @@ -800,12 +800,22 @@ func (p *GoPrinter) VisitPointerType(pt *tree.PointerType, param any) tree.J { func (p *GoPrinter) VisitChannel(ch *tree.Channel, param any) tree.J { out := param.(*PrintOutputCapture) p.beforeSyntax(ch.Prefix, ch.Markers, out) + + // Check for ChanDirMarker in markers for space before direction operator + var dirMarker tree.Space + if marker := tree.FindMarker[tree.ChanDirMarker](ch.Markers); marker != nil { + dirMarker = marker.Before + } + switch ch.Dir { case tree.ChanBidi: out.Append("chan") case tree.ChanSendOnly: - out.Append("chan<-") + out.Append("chan") + p.visitSpace(dirMarker, out) + out.Append("<-") case tree.ChanRecvOnly: + p.visitSpace(dirMarker, out) out.Append("<-chan") } p.Visit(ch.Value, out) diff --git a/rewrite-go/pkg/tree/go.go b/rewrite-go/pkg/tree/go.go index 9d4c1573b25..fd88d97357e 100644 --- a/rewrite-go/pkg/tree/go.go +++ b/rewrite-go/pkg/tree/go.go @@ -511,6 +511,15 @@ type ImportBlock struct { func (b ImportBlock) ID() uuid.UUID { return b.Ident } +// ChanDirMarker is a marker on a Channel that stores whitespace around the direction operator. +// For SEND channels, it stores the space before `<-`. For RECV channels, it's unused (space is in Prefix). +type ChanDirMarker struct { + Ident uuid.UUID + Before Space // space before the direction operator (<- or ->) +} + +func (c ChanDirMarker) ID() uuid.UUID { return c.Ident } + // MultiAssignment represents a multi-value assignment: `x, y = 1, 2` or `x, y := f()`. type MultiAssignment struct { ID uuid.UUID diff --git a/rewrite-go/src/main/java/org/openrewrite/golang/tree/ChanDirMarker.java b/rewrite-go/src/main/java/org/openrewrite/golang/tree/ChanDirMarker.java new file mode 100644 index 00000000000..26f40a6e981 --- /dev/null +++ b/rewrite-go/src/main/java/org/openrewrite/golang/tree/ChanDirMarker.java @@ -0,0 +1,45 @@ +/* + * Copyright 2026 the original author or authors. + *
+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *
+ * https://docs.moderne.io/licensing/moderne-source-available-license + *
+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.golang.tree; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.openrewrite.marker.Marker; +import org.openrewrite.java.tree.Space; + +import java.util.UUID; + +@Value +@EqualsAndHashCode(callSuper = false) +public class ChanDirMarker implements Marker { + UUID id; + Space before; + + public ChanDirMarker(UUID id, Space before) { + this.id = id; + this.before = before; + } + + @Override + public UUID getId() { + return id; + } + + @Override + public ChanDirMarker withId(UUID id) { + return new ChanDirMarker(id, this.before); + } +} diff --git a/rewrite-go/src/test/java/org/openrewrite/golang/ChanDirMarkerTest.java b/rewrite-go/src/test/java/org/openrewrite/golang/ChanDirMarkerTest.java new file mode 100644 index 00000000000..9ad50770957 --- /dev/null +++ b/rewrite-go/src/test/java/org/openrewrite/golang/ChanDirMarkerTest.java @@ -0,0 +1,136 @@ +/* + * Copyright 2026 the original author or authors. + *
+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *
+ * https://docs.moderne.io/licensing/moderne-source-available-license + *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openrewrite.golang;
+
+import org.junit.jupiter.api.Test;
+import org.openrewrite.golang.tree.ChanDirMarker;
+import org.openrewrite.golang.tree.Go;
+import org.openrewrite.test.RewriteTest;
+import org.openrewrite.test.SourceSpec;
+
+import java.util.function.Consumer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.openrewrite.golang.Assertions.go;
+
+class ChanDirMarkerTest implements RewriteTest {
+
+ @Test
+ void sendChannelWithSpaceBeforeArrow() {
+ rewriteRun(
+ go(
+ """
+ package main
+
+ func test() {
+ ch := make(chan <- int)
+ }
+ """,
+ spec -> spec.afterRecipe(cu -> {
+ // Verify that ChanDirMarker is present and captures the space
+ var channelType = findChannelType(cu);
+ assertThat(channelType).isNotNull();
+
+ var marker = channelType.getMarkers()
+ .findFirst(ChanDirMarker.class);
+ assertThat(marker).isPresent()
+ .hasValueSatisfying(m -> {
+ assertThat(m.getBefore().getWhitespace()).contains(" ");
+ });
+ })
+ )
+ );
+ }
+
+ @Test
+ void receiveChannelWithSpaceBeforeArrow() {
+ rewriteRun(
+ go(
+ """
+ package main
+
+ func test() {
+ ch := make(<- chan int)
+ }
+ """,
+ spec -> spec.afterRecipe(cu -> {
+ var channelType = findChannelType(cu);
+ assertThat(channelType).isNotNull();
+
+ // For receive channels, the space is typically in the prefix
+ // but may also be in ChanDirMarker depending on implementation
+ var marker = channelType.getMarkers()
+ .findFirst(ChanDirMarker.class);
+ // Just verify the marker can be found if present
+ assertThat(marker).isPresent();
+ })
+ )
+ );
+ }
+
+ @Test
+ void channelWithCommentBeforeArrow() {
+ rewriteRun(
+ go(
+ """
+ package main
+
+ func test() {
+ ch := make(chan /* send */ <- int)
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void biDirectionalChannelNoArrow() {
+ rewriteRun(
+ go(
+ """
+ package main
+
+ func test() {
+ ch := make(chan int)
+ }
+ """,
+ spec -> spec.afterRecipe(cu -> {
+ var channelType = findChannelType(cu);
+ assertThat(channelType).isNotNull();
+
+ var marker = channelType.getMarkers()
+ .findFirst(ChanDirMarker.class);
+ // Bi-directional channels may not have a ChanDirMarker
+ assertThat(marker).isEmpty();
+ })
+ )
+ );
+ }
+
+ private Go.Channel findChannelType(Go.CompilationUnit cu) {
+ final Go.Channel[] result = {null};
+ new org.openrewrite.golang.GolangVisitor