Repository: ruby/prism
Pull request #2372 (2024-02-06 20:53:27)

More visitors and tests for RipperCompat
Part of issue #2354

More support for operators. Add basic support for ranges, parentheses and negative integers. Tests for each of these.

Full Ripper compatibility will require a lot more, but it seems good to start simple.

noahgibbs: Ooh, interesting. JRuby seems to get a different unary minus symbol here, :- rather than :-@.

Patch:
diff --git a/lib/prism/ripper_compat.rb b/lib/prism/ripper_compat.rb
index 5bd3088a..739d5875 100644
--- a/lib/prism/ripper_compat.rb
+++ b/lib/prism/ripper_compat.rb
@@ -108,17 +108,51 @@ module Prism
 
     # Visit a CallNode node.
     def visit_call_node(node)
-      if !node.message.match?(/^[[:alpha:]_]/) && node.opening_loc.nil? && node.arguments&.arguments&.length == 1
+      bounds(node.location)
+
+      if node.message.match?(/^[[:alpha:]_]/)
+        val = on_ident(node.message)
+
+        return on_vcall(val)
+      end
+
+      if node.opening_loc.nil?
         left = visit(node.receiver)
-        right = visit(node.arguments.arguments.first)
+        if node.arguments&.arguments&.length == 1
+          right = visit(node.arguments.arguments.first)
+
+          on_binary(left, node.name, right)
+        elsif !node.arguments || node.arguments.empty?
+          on_unary(node.name, left)
+        else
+          raise NotImplementedError, "More than two arguments for operator"
+        end
+      else
+        # This is a method call, not an operator
+        raise NotImplementedError, "Non-nil opening_loc"
+      end
+    end
 
-        bounds(node.location)
-        on_binary(left, node.name, right)
+    # Visit a RangeNode
+    def visit_range_node(node)
+      bounds(node.location)
+      left = visit(node.left)
+      right = visit(node.right)
+
+      if node.operator_loc.slice == "..."
+        on_dot3(left, right)
       else
-        raise NotImplementedError
+        on_dot2(left, right)
       end
     end
 
+    # Visit a ParenthesesNode
+    def visit_parentheses_node(node)
+      bounds(node.location)
+      val = visit(node.body)
+      on_paren(val)
+    end
+
     # Visit a FloatNode node.
     def visit_float_node(node)
       bounds(node.location)
@@ -133,8 +167,22 @@ module Prism
 
     # Visit an IntegerNode node.
     def visit_integer_node(node)
-      bounds(node.location)
-      on_int(node.slice)
+      text = node.slice
+      loc = node.location
+      if text[0] == "-"
+        # TODO: This is ugly. We test that a newline between - and 7 doesn't give an error, but this could still be bad somehow.
+        bounds_line_col(loc.start_line, loc.start_column + 1)
+        on_int_val = on_int(text[1..-1])
+        bounds(node.location)
+        if RUBY_ENGINE == "jruby"
+          on_unary(:-, on_int_val)
+        else
+          on_unary(:-@, on_int_val)
+        end
+      else
+        bounds(node.location)
+        on_int(text)
+      end
     end
 
     # Visit a RationalNode node.
@@ -184,6 +232,13 @@ module Prism
       @column = location.start_column
     end
 
+    # If we need to do something unusual, we can directly update the line number
+    # and column to reflect the current node.
+    def bounds_line_col(lineno, column)
+      @lineno = lineno
+      @column = column
+    end
+
     # Lazily initialize the parse result.
     def result
       @result ||= Prism.parse(source)


Test patch:
diff --git a/test/prism/ripper_compat_test.rb b/test/prism/ripper_compat_test.rb
index e30f3824..0bdef530 100644
--- a/test/prism/ripper_compat_test.rb
+++ b/test/prism/ripper_compat_test.rb
@@ -10,6 +10,32 @@ module Prism
       assert_equivalent("6 / 7; 8 % 9")
     end
 
+    def test_unary
+      assert_equivalent("-7")
+    end
+
+    def test_unary_parens
+      assert_equivalent("-(7)")
+      assert_equivalent("(-7)")
+      assert_equivalent("(-\n7)")
+    end
+
+    def test_binary_parens
+      assert_equivalent("(3 + 7) * 4")
+    end
+
+    def test_ident
+      assert_equivalent("foo")
+    end
+
+    def test_range
+      assert_equivalent("(...2)")
+      assert_equivalent("(..2)")
+      assert_equivalent("(1...2)")
+      assert_equivalent("(1..2)")
+      assert_equivalent("(foo..-7)")
+    end
+
     private
 
     def assert_equivalent(source)