diff --git a/rewrite-ruby/src/main/java/org/openrewrite/ruby/RubyParserVisitor.java b/rewrite-ruby/src/main/java/org/openrewrite/ruby/RubyParserVisitor.java index 9484b863c6..0b463088b6 100644 --- a/rewrite-ruby/src/main/java/org/openrewrite/ruby/RubyParserVisitor.java +++ b/rewrite-ruby/src/main/java/org/openrewrite/ruby/RubyParserVisitor.java @@ -1465,6 +1465,11 @@ public J visitCallNode(Nodes.CallNode node) { skip("."); } + // `Foo.(a)` elides the `call` that Prism still reports as the name + if (name.equals("call") && !peekKeywordAt("call", indexOfNextNonWhitespace(cursor))) { + markers = markers.add(new ImplicitCall(randomId())); + } + J.Identifier methodName = identifier(name); if (name.equals("new")) { return new J.NewClass( @@ -2048,7 +2053,16 @@ public J visitAssocSplatNode(Nodes.AssocSplatNode node) { private Rb.Hash hash(Space prefix, Nodes.Node[] elements, Nodes.@Nullable Node rest) { AtomicReference markers = new AtomicReference<>(Markers.EMPTY); Space before = whitespace(); - boolean braces = source.startsWith("{", cursor); + + List all = new ArrayList<>(Arrays.asList(elements)); + if (rest != null) { + all.add(rest); + } + + // a brace-less hash whose first key is itself a hash (`eq({} => 0)`) also starts with `{`, + // so the brace only belongs to this hash when it sits ahead of the first pair + boolean braces = source.startsWith("{", cursor) && + (all.isEmpty() || cursor < charStart(all.get(0))); Markers hashMarkers = Markers.EMPTY; if (braces) { skip("{"); @@ -2056,11 +2070,6 @@ private Rb.Hash hash(Space prefix, Nodes.Node[] elements, Nodes.@Nullable Node r hashMarkers = hashMarkers.add(new OmitParentheses(randomId())); } - List all = new ArrayList<>(Arrays.asList(elements)); - if (rest != null) { - all.add(rest); - } - List> pairs = new ArrayList<>(all.size()); for (int i = 0; i < all.size(); i++) { Expression pair = all.get(i) instanceof Nodes.AssocNode ? diff --git a/rewrite-ruby/src/main/java/org/openrewrite/ruby/internal/RubyPrinter.java b/rewrite-ruby/src/main/java/org/openrewrite/ruby/internal/RubyPrinter.java index 743db00d90..1645591c38 100644 --- a/rewrite-ruby/src/main/java/org/openrewrite/ruby/internal/RubyPrinter.java +++ b/rewrite-ruby/src/main/java/org/openrewrite/ruby/internal/RubyPrinter.java @@ -1223,7 +1223,11 @@ public J visitMethodInvocation(J.MethodInvocation method, PrintOutputCapture

(method.getMarkers().findFirst(Colon2.class).isPresent() ? "::" : "."); visitRightPadded(method.getPadding().getSelect(), JRightPadded.Location.METHOD_SELECT, suffix, p); - visit(method.getName(), p); + // `Foo.()` writes no message, but a recipe that renamed it has to write the new one + if (!method.getMarkers().findFirst(ImplicitCall.class).isPresent() || + !"call".equals(method.getSimpleName())) { + visit(method.getName(), p); + } JContainer args = method.getPadding().getArguments(); AtomicReference blockArg = new AtomicReference<>(); @@ -1308,7 +1312,8 @@ public J visitNewClass(J.NewClass newClass, PrintOutputCapture

p) { beforeSyntax(newClass, Space.Location.NEW_CLASS_PREFIX, p); visit(newClass.getClazz(), p); visitSpace(requireNonNull(newClass.getPadding().getEnclosing()).getAfter(), Space.Location.NEW_CLASS_ENCLOSING_SUFFIX, p); - p.append(newClass.getMarkers().findFirst(SafeNavigation.class).isPresent() ? "&." : "."); + p.append((newClass.getMarkers().findFirst(SafeNavigation.class).isPresent() ? "&" : "") + + (newClass.getMarkers().findFirst(Colon2.class).isPresent() ? "::" : ".")); visitSpace(newClass.getNew(), Space.Location.NEW_PREFIX, p); p.append("new"); JContainer args = newClass.getPadding().getArguments(); diff --git a/rewrite-ruby/src/main/java/org/openrewrite/ruby/marker/ImplicitCall.java b/rewrite-ruby/src/main/java/org/openrewrite/ruby/marker/ImplicitCall.java new file mode 100644 index 0000000000..1f038fbb6b --- /dev/null +++ b/rewrite-ruby/src/main/java/org/openrewrite/ruby/marker/ImplicitCall.java @@ -0,0 +1,31 @@ +/* + * 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.ruby.marker; + +import lombok.Value; +import lombok.With; +import org.openrewrite.marker.Marker; + +import java.util.UUID; + +/** + * {@code Foo.(a)} is shorthand for {@code Foo.call(a)}, written with the message elided. + */ +@Value +@With +public class ImplicitCall implements Marker { + UUID id; +} diff --git a/rewrite-ruby/src/test/java/org/openrewrite/ruby/tree/HashTest.java b/rewrite-ruby/src/test/java/org/openrewrite/ruby/tree/HashTest.java index e52c44b697..7cbb731422 100644 --- a/rewrite-ruby/src/test/java/org/openrewrite/ruby/tree/HashTest.java +++ b/rewrite-ruby/src/test/java/org/openrewrite/ruby/tree/HashTest.java @@ -156,4 +156,17 @@ void bracketedConstructorWithoutTrailingComma() { ) ); } + + @Test + void hashKey() { + rewriteRun( + ruby( + """ + expect(metrics[0].data).to eq({} => 0) + expect(metrics[0].data).to eq({a: 1} => 0) + x = {{} => 0} + """ + ) + ); + } } diff --git a/rewrite-ruby/src/test/java/org/openrewrite/ruby/tree/MethodInvocationTest.java b/rewrite-ruby/src/test/java/org/openrewrite/ruby/tree/MethodInvocationTest.java index 79cb528a21..7d1cf0ed3d 100644 --- a/rewrite-ruby/src/test/java/org/openrewrite/ruby/tree/MethodInvocationTest.java +++ b/rewrite-ruby/src/test/java/org/openrewrite/ruby/tree/MethodInvocationTest.java @@ -79,6 +79,57 @@ void blockLastArgument() { ); } + @Test + void callSugar() { + rewriteRun( + ruby( + """ + Sweep.() + MarkForToken.(t) + MarkForToken.(t, 1) + obj&.() + """ + ) + ); + } + + @Test + void callSugarWithBlock() { + rewriteRun( + ruby( + """ + Sweep.() { |a| a } + """ + ) + ); + } + + @Test + void explicitCall() { + rewriteRun( + ruby( + """ + Sweep.call() + Sweep.call + MarkForToken.call(t) + """ + ) + ); + } + + @Test + void colon2Call() { + rewriteRun( + ruby( + """ + Nokogiri::XML(response.body) + WEBrick::Log::new(log_path) + Integer::sqrt(9) + """ + ) + ); + } + @Test void noParens() { rewriteRun(