Repository: ruby/yarp
Pull request #1359 (2023-08-30 14:47:25)

Fixing end-of-file issues found by the fuzzer
## What's changed

I've introduced a new test file, `test/yarp/fuzzer_test.rb` to exercise the snippets. These snippets all caused invalid memory access that is caught by valgrind.

You can run these tests with:

```
bundle exec rake test:valgrind TESTOPTS=-n/fuzzer/
```

I've also enabled valgrind debugging for `YARP.dump` similar to what has been done in `YARP.parse`.

The fixes here are mostly small and should be uncontroversial.

## Reviewers should pay attention to ...

The commit fixing "$" at the end of a file. I've introduced a label and some `goto`s to unify error handling in `lex_global_variable`. This may violate the informal style guide and/or your sensibilities, in which case let me know and I'll change it.
 


Patch:
diff --git a/ext/yarp/extension.c b/ext/yarp/extension.c
index b46a1f82..f959dba3 100644
--- a/ext/yarp/extension.c
+++ b/ext/yarp/extension.c
@@ -83,7 +83,21 @@ dump(int argc, VALUE *argv, VALUE self) {
 
     yp_string_t input;
     input_load_string(&input, string);
-    return dump_input(&input, check_string(filepath));
+
+#ifdef YARP_DEBUG_MODE_BUILD
+    size_t length = yp_string_length(&input);
+    char* dup = malloc(length);
+    memcpy(dup, yp_string_source(&input), length);
+    yp_string_constant_init(&input, dup, length);
+#endif
+
+    VALUE value = dump_input(&input, check_string(filepath));
+
+#ifdef YARP_DEBUG_MODE_BUILD
+    free(dup);
+#endif
+
+    return value;
 }
 
 // Dump the AST corresponding to the given file to a string.
diff --git a/lib/yarp/desugar_visitor.rb b/lib/yarp/desugar_visitor.rb
index 8fe48836..83a9bb53 100644
--- a/lib/yarp/desugar_visitor.rb
+++ b/lib/yarp/desugar_visitor.rb
@@ -8,16 +8,26 @@ module YARP
     #
     # @@foo && @@foo = bar
     def visit_class_variable_and_write_node(node)
-      desugar_and_write_node(node, ClassVariableReadNode, ClassVariableWriteNode)
+      AndNode.new(
+        ClassVariableReadNode.new(node.name_loc),
+        ClassVariableWriteNode.new(node.name_loc, node.value, node.operator_loc, node.location),
+        node.operator_loc,
+        node.location
+      )
     end
 
     # @@foo ||= bar
     #
     # becomes
     #
-    # defined?(@@foo) ? @@foo : @@foo = bar
+    # @@foo || @@foo = bar
     def visit_class_variable_or_write_node(node)
-      desugar_or_write_defined_node(node, ClassVariableReadNode, ClassVariableWriteNode)
+      OrNode.new(
+        ClassVariableReadNode.new(node.name_loc),
+        ClassVariableWriteNode.new(node.name_loc, node.value, node.operator_loc, node.location),
+        node.operator_loc,
+        node.location
+      )
     end
 
     # @@foo += bar
@@ -26,7 +36,7 @@ module YARP
     #
     # @@foo = @@foo + bar
     def visit_class_variable_operator_write_node(node)
-      desugar_operator_write_node(node, ClassVariableReadNode, ClassVariableWriteNode)
+      desugar_operator_write_node(node, ClassVariableWriteNode, ClassVariableReadNode)
     end
 
     # Foo &&= bar
@@ -35,7 +45,12 @@ module YARP
     #
     # Foo && Foo = bar
     def visit_constant_and_write_node(node)
-      desugar_and_write_node(node, ConstantReadNode, ConstantWriteNode)
+      AndNode.new(
+        ConstantReadNode.new(node.name_loc),
+        ConstantWriteNode.new(node.name_loc, node.value, node.operator_loc, node.location),
+        node.operator_loc,
+        node.location
+      )
     end
 
     # Foo ||= bar
@@ -44,7 +59,12 @@ module YARP
     #
     # Foo || Foo = bar
     def visit_constant_or_write_node(node)
-      desugar_or_write_defined_node(node, ConstantReadNode, ConstantWriteNode)
+      OrNode.new(
+        ConstantReadNode.new(node.name_loc),
+        ConstantWriteNode.new(node.name_loc, node.value, node.operator_loc, node.location),
+        node.operator_loc,
+        node.location
+      )
     end
 
     # Foo += bar
@@ -53,7 +73,7 @@ module YARP
     #
     # Foo = Foo + bar
     def visit_constant_operator_write_node(node)
-      desugar_operator_write_node(node, ConstantReadNode, ConstantWriteNode)
+      desugar_operator_write_node(node, ConstantWriteNode, ConstantReadNode)
     end
 
     # Foo::Bar &&= baz
@@ -76,19 +96,9 @@ module YARP
     #
     # Foo::Bar || Foo::Bar = baz
     def visit_constant_path_or_write_node(node)
-      IfNode.new(
-        node.operator_loc,
-        DefinedNode.new(nil, node.target, nil, node.operator_loc, node.target.location),
-        StatementsNode.new([node.target], node.location),
-        ElseNode.new(
-          node.operator_loc,
-          StatementsNode.new(
-            [ConstantPathWriteNode.new(node.target, node.value, node.operator_loc, node.location)],
-            node.location
-          ),
-          node.operator_loc,
-          node.location
-        ),
+      OrNode.new(
+        node.target,
+        ConstantPathWriteNode.new(node.target, node.value, node.operator_loc, node.location),
         node.operator_loc,
         node.location
       )
@@ -125,7 +135,12 @@ module YARP
     #
     # $foo && $foo = bar
     def visit_global_variable_and_write_node(node)
-      desugar_and_write_node(node, GlobalVariableReadNode, GlobalVariableWriteNode)
+      AndNode.new(
+        GlobalVariableReadNode.new(node.name_loc),
+        GlobalVariableWriteNode.new(node.name_loc, node.value, node.operator_loc, node.location),
+        node.operator_loc,
+        node.location
+      )
     end
 
     # $foo ||= bar
@@ -134,7 +149,12 @@ module YARP
     #
     # $foo || $foo = bar
     def visit_global_variable_or_write_node(node)
-      desugar_or_write_defined_node(node, GlobalVariableReadNode, GlobalVariableWriteNode)
+      OrNode.new(
+        GlobalVariableReadNode.new(node.name_loc),
+        GlobalVariableWriteNode.new(node.name_loc, node.value, node.operator_loc, node.location),
+        node.operator_loc,
+        node.location
+      )
     end
 
     # $foo += bar
@@ -143,7 +163,7 @@ module YARP
     #
     # $foo = $foo + bar
     def visit_global_variable_operator_write_node(node)
-      desugar_operator_write_node(node, GlobalVariableReadNode, GlobalVariableWriteNode)
+      desugar_operator_write_node(node, GlobalVariableWriteNode, GlobalVariableReadNode)
     end
 
     # @foo &&= bar
@@ -152,7 +172,12 @@ module YARP
     #
     # @foo && @foo = bar
     def visit_instance_variable_and_write_node(node)
-      desugar_and_write_node(node, InstanceVariableReadNode, InstanceVariableWriteNode, arguments: [node.name])
+      AndNode.new(
+        InstanceVariableReadNode.new(node.name, node.name_loc),
+        InstanceVariableWriteNode.new(node.name, node.name_loc, node.value, node.operator_loc, node.location),
+        node.operator_loc,
+        node.location
+      )
     end
 
     # @foo ||= bar
@@ -161,7 +186,12 @@ module YARP
     #
     # @foo || @foo = bar
     def visit_instance_variable_or_write_node(node)
-      desugar_or_write_node(node, InstanceVariableReadNode, InstanceVariableWriteNode, arguments: [node.name])
+      OrNode.new(
+        InstanceVariableReadNode.new(node.name, node.name_loc),
+        InstanceVariableWriteNode.new(node.name, node.name_loc, node.value, node.operator_loc, node.location),
+        node.operator_loc,
+        node.location
+      )
     end
 
     # @foo += bar
@@ -170,7 +200,7 @@ module YARP
     #
     # @foo = @foo + bar
     def visit_instance_variable_operator_write_node(node)
-      desugar_operator_write_node(node, InstanceVariableReadNode, InstanceVariableWriteNode, arguments: [node.name])
+      desugar_operator_write_node(node, InstanceVariableWriteNode, InstanceVariableReadNode, arguments: [node.name])
     end
 
     # foo &&= bar
@@ -179,7 +209,12 @@ module YARP
     #
     # foo && foo = bar
     def visit_local_variable_and_write_node(node)
-      desugar_and_write_node(node, LocalVariableReadNode, LocalVariableWriteNode, arguments: [node.name, node.depth])
+      AndNode.new(
+        LocalVariableReadNode.new(node.name, node.depth, node.name_loc),
+        LocalVariableWriteNode.new(node.name, node.depth, node.name_loc, node.value, node.operator_loc, node.location),
+        node.operator_loc,
+        node.location
+      )
     end
 
     # foo ||= bar
@@ -188,7 +223,12 @@ module YARP
     #
     # foo || foo = bar
     def visit_local_variable_or_write_node(node)
-      desugar_or_write_node(node, LocalVariableReadNode, LocalVariableWriteNode, arguments: [node.name, node.depth])
+      OrNode.new(
+        LocalVariableReadNode.new(node.name, node.depth, node.name_loc),
+        LocalVariableWriteNode.new(node.name, node.depth, node.name_loc, node.value, node.operator_loc, node.location),
+        node.operator_loc,
+        node.location
+      )
     end
 
     # foo += bar
@@ -197,23 +237,13 @@ module YARP
     #
     # foo = foo + bar
     def visit_local_variable_operator_write_node(node)
-      desugar_operator_write_node(node, LocalVariableReadNode, LocalVariableWriteNode, arguments: [node.name, node.depth])
+      desugar_operator_write_node(node, LocalVariableWriteNode, LocalVariableReadNode, arguments: [node.name, node.depth])
     end
 
     private
 
-    # Desugar `x &&= y` to `x && x = y`
-    def desugar_and_write_node(node, read_class, write_class, arguments: [])
-      AndNode.new(
-        read_class.new(*arguments, node.name_loc),
-        write_class.new(*arguments, node.name_loc, node.value, node.operator_loc, node.location),
-        node.operator_loc,
-        node.location
-      )
-    end
-
     # Desugar `x += y` to `x = x + y`
-    def desugar_operator_write_node(node, read_class, write_class, arguments: [])
+    def desugar_operator_write_node(node, write_class, read_class, arguments: [])
       write_class.new(
         *arguments,
         node.name_loc,
@@ -233,35 +263,5 @@ module YARP
         node.location
       )
     end
-
-    # Desugar `x ||= y` to `x || x = y`
-    def desugar_or_write_node(node, read_class, write_class, arguments: [])
-      OrNode.new(
-        read_class.new(*arguments, node.name_loc),
-        write_class.new(*arguments, node.name_loc, node.value, node.operator_loc, node.location),
-        node.operator_loc,
-        node.location
-      )
-    end
-
-    # Don't desugar `x ||= y` to `defined?(x) ? x : x = y`
-    def desugar_or_write_defined_node(node, read_class, write_class)
-      IfNode.new(
-        node.operator_loc,
-        DefinedNode.new(nil, read_class.new(node.name_loc), nil, node.operator_loc, node.name_loc),
-        StatementsNode.new([read_class.new(node.name_loc)], node.location),
-        ElseNode.new(
-          node.operator_loc,
-          StatementsNode.new(
-            [write_class.new(node.name_loc, node.value, node.operator_loc, node.location)],
-            node.location
-          ),
-          node.operator_loc,
-          node.location
-        ),
-        node.operator_loc,
-        node.location
-      )
-    end
   end
 end
diff --git a/rust/yarp/build.rs b/rust/yarp/build.rs
index 3e15175f..83ca334e 100644
--- a/rust/yarp/build.rs
+++ b/rust/yarp/build.rs
@@ -675,17 +675,6 @@ impl<'pr> Node<'pr> {{
     writeln!(file, "    }}")?;
     writeln!(file)?;
 
-    writeln!(file, "    /// Returns the location of this node.")?;
-    writeln!(file, "    #[must_use]")?;
-    writeln!(file, "    pub fn location(&self) -> Location<'pr> {{")?;
-    writeln!(file, "        match *self {{")?;
-    for node in &config.nodes {
-        writeln!(file, "            Self::{} {{ pointer, .. }} => Location {{ pointer: unsafe {{ NonNull::new_unchecked(&mut (*pointer.cast::<yp_node_t>()).location) }}, marker: PhantomData }},", node.name)?;
-    }
-    writeln!(file, "        }}")?;
-    writeln!(file, "    }}")?;
-    writeln!(file)?;
-
     for node in &config.nodes {
         writeln!(file, "    /// Returns the node as a `{}`.", node.name)?;
         writeln!(file, "    #[must_use]")?;
diff --git a/rust/yarp/src/lib.rs b/rust/yarp/src/lib.rs
index 21059a06..6d47eb5a 100644
--- a/rust/yarp/src/lib.rs
+++ b/rust/yarp/src/lib.rs
@@ -252,11 +252,6 @@ mod tests {
 
         assert_eq!(slice, "222");
 
-        let location = node.location();
-        let slice = std::str::from_utf8(result.as_slice(&location)).unwrap();
-
-        assert_eq!(slice, "222");
-
         let slice = std::str::from_utf8(location.as_slice()).unwrap();
 
         assert_eq!(slice, "222");
diff --git a/src/yarp.c b/src/yarp.c
index 089de42b..2422fb45 100644
--- a/src/yarp.c
+++ b/src/yarp.c
@@ -380,6 +380,9 @@ lex_state_arg_p(yp_parser_t *parser) {
 
 static inline bool
 lex_state_spcarg_p(yp_parser_t *parser, bool space_seen) {
+    if (parser->current.end >= parser->end) {
+        return false;
+    }
     return lex_state_arg_p(parser) && space_seen && !yp_char_is_whitespace(*parser->current.end);
 }
 
@@ -4994,7 +4997,8 @@ lex_numeric_prefix(yp_parser_t *parser) {
             // 0d1111 is a decimal number
             case 'd':
             case 'D':
-                if (yp_char_is_decimal_digit(*++parser->current.end)) {
+                parser->current.end++;
+                if (yp_char_is_decimal_digit(peek(parser))) {
                     parser->current.end += yp_strspn_decimal_number(parser->current.end, parser->end - parser->current.end);
                 } else {
                     yp_diagnostic_list_append(&parser->error_list, parser->current.start, parser->current.end, "Invalid decimal number.");
@@ -5005,7 +5009,8 @@ lex_numeric_prefix(yp_parser_t *parser) {
             // 0b1111 is a binary number
             case 'b':
             case 'B':
-                if (yp_char_is_binary_digit(*++parser->current.end)) {
+                parser->current.end++;
+                if (yp_char_is_binary_digit(peek(parser))) {
                     parser->current.end += yp_strspn_binary_number(parser->current.end, parser->end - parser->current.end);
                 } else {
                     yp_diagnostic_list_append(&parser->error_list, parser->current.start, parser->current.end, "Invalid binary number.");
@@ -5016,7 +5021,8 @@ lex_numeric_prefix(yp_parser_t *parser) {
             // 0o1111 is an octal number
             case 'o':
             case 'O':
-                if (yp_char_is_octal_digit(*++parser->current.end)) {
+                parser->current.end++;
+                if (yp_char_is_octal_digit(peek(parser))) {
                     parser->current.end += yp_strspn_octal_number(parser->current.end, parser->end - parser->current.end);
                 } else {
                     yp_diagnostic_list_append(&parser->error_list, parser->current.start, parser->current.end, "Invalid octal number.");
@@ -5040,7 +5046,8 @@ lex_numeric_prefix(yp_parser_t *parser) {
             // 0x1111 is a hexadecimal number
             case 'x':
             case 'X':
-                if (yp_char_is_hexadecimal_digit(*++parser->current.end)) {
+                parser->current.end++;
+                if (yp_char_is_hexadecimal_digit(peek(parser))) {
                     parser->current.end += yp_strspn_hexadecimal_number(parser->current.end, parser->end - parser->current.end);
                 } else {
                     yp_diagnostic_list_append(&parser->error_list, parser->current.start, parser->current.end, "Invalid hexadecimal number.");
@@ -5124,6 +5131,11 @@ lex_numeric(yp_parser_t *parser) {
 
 static yp_token_type_t
 lex_global_variable(yp_parser_t *parser) {
+    if (parser->current.end >= parser->end) {
+        yp_diagnostic_list_append(&parser->error_list, parser->current.start, parser->current.end, "Invalid global variable.");
+        return YP_TOKEN_GLOBAL_VARIABLE;
+    }
+
     switch (*parser->current.end) {
         case '~':  // $~: match-data
         case '*':  // $*: argv
@@ -5903,13 +5915,13 @@ parser_lex(yp_parser_t *parser) {
                             // Here we look for a "." or "&." following a "\n".
                             const char *following = next_newline(next_content, parser->end - next_content);
 
-                            while (following && (following < parser->end)) {
+                            while (following && (following + 1 < parser->end)) {
                                 following++;
                                 following += yp_strspn_inline_whitespace(following, parser->end - following);
 
                                 // If this is not followed by a comment, then we can break out
                                 // of this loop.
-                                if (*following != '#') break;
+                                if (peek_at(parser, following) != '#') break;
 
                                 // If there is a comment, then we need to find the end of the
                                 // comment and continue searching from there.
@@ -6576,7 +6588,7 @@ parser_lex(yp_parser_t *parser) {
                         LEX(YP_TOKEN_COLON_COLON);
                     }
 
-                    if (lex_state_end_p(parser) || yp_char_is_whitespace(*parser->current.end) || peek(parser) == '#') {
+                    if (lex_state_end_p(parser) || yp_char_is_whitespace(peek(parser)) || peek(parser) == '#') {
                         lex_state_set(parser, YP_LEX_STATE_BEG);
                         LEX(YP_TOKEN_COLON);
                     }
@@ -6940,6 +6952,12 @@ parser_lex(yp_parser_t *parser) {
                 // literally. In this case we'll skip past the next character
                 // and find the next breakpoint.
                 if (*breakpoint == '\\') {
+                    // Check that we're not at the end of the file.
+                    if (breakpoint + 1 >= parser->end) {
+                        breakpoint = NULL;
+                        continue;
+                    }
+
                     yp_unescape_type_t unescape_type = lex_mode->as.list.interpolation ? YP_UNESCAPE_ALL : YP_UNESCAPE_MINIMAL;
                     size_t difference = yp_unescape_calculate_difference(parser, breakpoint, unescape_type, false);
 
@@ -7073,6 +7091,12 @@ parser_lex(yp_parser_t *parser) {
                 // literally. In this case we'll skip past the next character
                 // and find the next breakpoint.
                 if (*breakpoint == '\\') {
+                    // Check that we're not at the end of the file.
+                    if (breakpoint + 1 >= parser->end) {
+                        breakpoint = NULL;
+                        continue;
+                    }
+
                     size_t difference = yp_unescape_calculate_difference(parser, breakpoint, YP_UNESCAPE_ALL, false);
 
                     // If the result is an escaped newline ...
@@ -7219,6 +7243,12 @@ parser_lex(yp_parser_t *parser) {
                         breakpoint = yp_strpbrk(parser, breakpoint + 1, breakpoints, parser->end - (breakpoint + 1));
                         break;
                     case '\\': {
+                        // Check that we're not at the end of the file.
+                        if (breakpoint + 1 >= parser->end) {
+                            breakpoint = NULL;
+                            break;
+                        }
+
                         // If we hit escapes, then we need to treat the next token
                         // literally. In this case we'll skip past the next character and
                         // find the next breakpoint.
@@ -7383,6 +7413,12 @@ parser_lex(yp_parser_t *parser) {
                         break;
                     }
                     case '\\': {
+                        // Check that we're not at the end of the file.
+                        if (breakpoint + 1 >= parser->end) {
+                            breakpoint = NULL;
+                            break;
+                        }
+
                         // If we hit an escape, then we need to skip past
                         // however many characters the escape takes up. However
                         // it's important that if \n or \r\n are escaped that we


Test patch:
diff --git a/test/yarp/desugar_visitor_test.rb b/test/yarp/desugar_visitor_test.rb
index de635966..a893422a 100644
--- a/test/yarp/desugar_visitor_test.rb
+++ b/test/yarp/desugar_visitor_test.rb
@@ -14,10 +14,10 @@ class DesugarVisitorTest < Test::Unit::TestCase
   end
 
   def test_or_write
-    assert_desugars("(IfNode (DefinedNode (ClassVariableReadNode)) (StatementsNode (ClassVariableReadNode)) (ElseNode (StatementsNode (ClassVariableWriteNode (CallNode)))))", "@@foo ||= bar")
-    assert_desugars("(IfNode (DefinedNode (ConstantPathNode (ConstantReadNode) (ConstantReadNode))) (StatementsNode (ConstantPathNode (ConstantReadNode) (ConstantReadNode))) (ElseNode (StatementsNode (ConstantPathWriteNode (ConstantPathNode (ConstantReadNode) (ConstantReadNode)) (CallNode)))))", "Foo::Bar ||= baz")
-    assert_desugars("(IfNode (DefinedNode (ConstantReadNode)) (StatementsNode (ConstantReadNode)) (ElseNode (StatementsNode (ConstantWriteNode (CallNode)))))", "Foo ||= bar")
-    assert_desugars("(IfNode (DefinedNode (GlobalVariableReadNode)) (StatementsNode (GlobalVariableReadNode)) (ElseNode (StatementsNode (GlobalVariableWriteNode (CallNode)))))", "$foo ||= bar")
+    assert_desugars("(OrNode (ClassVariableReadNode) (ClassVariableWriteNode (CallNode)))", "@@foo ||= bar")
+    assert_desugars("(OrNode (ConstantPathNode (ConstantReadNode) (ConstantReadNode)) (ConstantPathWriteNode (ConstantPathNode (ConstantReadNode) (ConstantReadNode)) (CallNode)))", "Foo::Bar ||= baz")
+    assert_desugars("(OrNode (ConstantReadNode) (ConstantWriteNode (CallNode)))", "Foo ||= bar")
+    assert_desugars("(OrNode (GlobalVariableReadNode) (GlobalVariableWriteNode (CallNode)))", "$foo ||= bar")
     assert_desugars("(OrNode (InstanceVariableReadNode) (InstanceVariableWriteNode (CallNode)))", "@foo ||= bar")
     assert_desugars("(OrNode (LocalVariableReadNode) (LocalVariableWriteNode (CallNode)))", "foo ||= bar")
     assert_desugars("(OrNode (LocalVariableReadNode) (LocalVariableWriteNode (CallNode)))", "foo = 1; foo ||= bar")
diff --git a/test/yarp/errors_test.rb b/test/yarp/errors_test.rb
index 074dd712..e461afbb 100644
--- a/test/yarp/errors_test.rb
+++ b/test/yarp/errors_test.rb
@@ -1106,6 +1106,12 @@ class ErrorsTest < Test::Unit::TestCase
     assert_errors expected, "def foo(a = 1,b,*c);end", [["Unexpected parameter *", 16..17]]
   end
 
+  def test_unterminated_global_variable
+    assert_errors expression("$"), "$", [
+      ["Invalid global variable.", 0..1]
+    ]
+  end
+
   private
 
   def assert_errors(expected, source, errors)
diff --git a/test/yarp/fuzzer_test.rb b/test/yarp/fuzzer_test.rb
new file mode 100644
index 00000000..8d818897
--- /dev/null
+++ b/test/yarp/fuzzer_test.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require_relative "test_helper"
+
+# These tests are simply to exercise snippets found by the fuzzer that caused invalid memory access.
+class FuzzerTest < Test::Unit::TestCase
+  class << self
+    def snippet(name, source)
+      test "fuzzer #{name}" do
+        YARP.dump(source)
+      end
+    end
+  end
+
+  snippet "incomplete global variable", "$"
+  snippet "incomplete symbol", ":"
+  snippet "incomplete escaped string", '"\\'
+  snippet "trailing comment", "1\n#\n"
+  snippet "comment followed by whitespace at end of file", "1\n#\n "
+  snippet "trailing asterisk", "a *"
+  snippet "incomplete decimal number", "0d"
+  snippet "incomplete binary number", "0b"
+  snippet "incomplete octal number", "0o"
+  snippet "incomplete hex number", "0x"
+  snippet "incomplete escaped list", "%w[\\"
+  snippet "incomplete escaped regex", "/a\\"
+  snippet "unterminated heredoc with unterminated escape at end of file", "<<A\n\\"
+end
diff --git a/test/yarp/ruby_api_test.rb b/test/yarp/ruby_api_test.rb
index 236407cc..f0202654 100644
--- a/test/yarp/ruby_api_test.rb
+++ b/test/yarp/ruby_api_test.rb
@@ -31,30 +31,6 @@ class YARPRubyAPITest < Test::Unit::TestCase
     assert_equal 0.5ri, parse_expression("0.5ri").value
   end
 
-  def test_location_join
-    recv, args_node, _ = parse_expression("1234 + 567").child_nodes
-    arg = args_node.arguments[0]
-
-    joined = recv.location.join(arg.location)
-    assert_equal 0, joined.start_offset
-    assert_equal 10, joined.length
-
-    assert_raise RuntimeError, "Incompatible locations" do
-      arg.location.join(recv.location)
-    end
-
-    other_recv, other_args_node, _ = parse_expression("1234 + 567").child_nodes
-    other_arg = other_args_node.arguments[0]
-
-    assert_raise RuntimeError, "Incompatible sources" do
-      other_arg.location.join(recv.location)
-    end
-
-    assert_raise RuntimeError, "Incompatible sources" do
-      recv.location.join(other_arg.location)
-    end
-  end
-
   private
 
   def parse_expression(source)