Repository: ruby/prism (c)

Issue:
YARP does not parse `/splitted-by-heredoc-named-capture-regexp/ =~ string` correctly
This code has MatchWriteNode
```ruby
<<A
A
/\
(?<a>)/ =~ ''
a #=> ""
```

This code should have MatchWriteNode but it doesn't
```ruby
<<A; /\
A
(?<a>)/ =~ ''
a #=> ""
```

Result of two codes
```ruby
#<YARP::ParseResult:0x00000001059fdfc0
 @comments=[#<YARP::Comment @type=:inline @location=#<YARP::Location @start_offset=25 @length=6>>],
 @errors=[],
 @source=#<YARP::Source:0x00000001059d89f0 @offsets=[0, 4, 6, 9, 23], @source="<<A\nA\n/\\\n(?<a>)/ =~ ''\na #=> \"\"">,
 @value=
  @ ProgramNode (location: (0...24))
├── locals: [:a]
└── statements:
    @ StatementsNode (location: (0...24))
    └── body: (length: 3)
        ├── @ StringNode (location: (0...3))
        │   ├── flags: ∅
        │   ├── opening_loc: (0...3) = "<<A"
        │   ├── content_loc: (4...4) = ""
        │   ├── closing_loc: (4...6) = "A\n"
        │   └── unescaped: ""
        ├── @ MatchWriteNode (location: (6...22))
        │   ├── call:
        │   │   @ CallNode (location: (6...22))
        │   │   ├── receiver:
        │   │   │   @ RegularExpressionNode (location: (6...16))
        │   │   │   ├── opening_loc: (6...7) = "/"
        │   │   │   ├── content_loc: (7...15) = "\\\n(?<a>)"
        │   │   │   ├── closing_loc: (15...16) = "/"
        │   │   │   ├── unescaped: "(?<a>)"
        │   │   │   └── flags: ∅
        │   │   ├── call_operator_loc: ∅
        │   │   ├── message_loc: (17...19) = "=~"
        │   │   ├── opening_loc: ∅
        │   │   ├── arguments:
        │   │   │   @ ArgumentsNode (location: (20...22))
        │   │   │   └── arguments: (length: 1)
        │   │   │       └── @ StringNode (location: (20...22))
        │   │   │           ├── flags: ∅
        │   │   │           ├── opening_loc: (20...21) = "'"
        │   │   │           ├── content_loc: (21...21) = ""
        │   │   │           ├── closing_loc: (21...22) = "'"
        │   │   │           └── unescaped: ""
        │   │   ├── closing_loc: ∅
        │   │   ├── block: ∅
        │   │   ├── flags: ∅
        │   │   └── name: "=~"
        │   └── locals: [:a]
        └── @ LocalVariableReadNode (location: (23...24))
            ├── name: :a
            └── depth: 0,
 @warnings=[]>
```

```ruby
#<YARP::ParseResult:0x000000010585f830
 @comments=[#<YARP::Comment @type=:inline @location=#<YARP::Location @start_offset=26 @length=6>>],
 @errors=[],
 @source=#<YARP::Source:0x0000000105830968 @offsets=[0, 8, 10, 24], @source="<<A; /\\\nA\n(?<a>)/ =~ ''\na #=> \"\"">,
 @value=
  @ ProgramNode (location: (0...25))
├── locals: []
└── statements:
    @ StatementsNode (location: (0...25))
    └── body: (length: 3)
        ├── @ StringNode (location: (0...3))
        │   ├── flags: ∅
        │   ├── opening_loc: (0...3) = "<<A"
        │   ├── content_loc: (8...8) = ""
        │   ├── closing_loc: (8...10) = "A\n"
        │   └── unescaped: ""
        ├── @ CallNode (location: (5...23))
        │   ├── receiver:
        │   │   @ InterpolatedRegularExpressionNode (location: (5...17))
        │   │   ├── opening_loc: (5...6) = "/"
        │   │   ├── parts: (length: 2)
        │   │   │   ├── @ StringNode (location: (6...8))
        │   │   │   │   ├── flags: ∅
        │   │   │   │   ├── opening_loc: ∅
        │   │   │   │   ├── content_loc: (6...8) = "\\\n"
        │   │   │   │   ├── closing_loc: ∅
        │   │   │   │   └── unescaped: ""
        │   │   │   └── @ StringNode (location: (10...16))
        │   │   │       ├── flags: ∅
        │   │   │       ├── opening_loc: ∅
        │   │   │       ├── content_loc: (10...16) = "(?<a>)"
        │   │   │       ├── closing_loc: ∅
        │   │   │       └── unescaped: "(?<a>)"
        │   │   ├── closing_loc: (16...17) = "/"
        │   │   └── flags: ∅
        │   ├── call_operator_loc: ∅
        │   ├── message_loc: (18...20) = "=~"
        │   ├── opening_loc: ∅
        │   ├── arguments:
        │   │   @ ArgumentsNode (location: (21...23))
        │   │   └── arguments: (length: 1)
        │   │       └── @ StringNode (location: (21...23))
        │   │           ├── flags: ∅
        │   │           ├── opening_loc: (21...22) = "'"
        │   │           ├── content_loc: (22...22) = ""
        │   │           ├── closing_loc: (22...23) = "'"
        │   │           └── unescaped: ""
        │   ├── closing_loc: ∅
        │   ├── block: ∅
        │   ├── flags: ∅
        │   └── name: "=~"
        └── @ CallNode (location: (24...25))
            ├── receiver: ∅
            ├── call_operator_loc: ∅
            ├── message_loc: (24...25) = "a"
            ├── opening_loc: ∅
            ├── arguments: ∅
            ├── closing_loc: ∅
            ├── block: ∅
            ├── flags: variable_call
            └── name: "a",
 @warnings=[]>
```

Pull request:
Use MatchWriteNode on split InterpolatedREN
Fixes #1535

Patch:
diff --git a/src/prism.c b/src/prism.c
index 9f70a1e2..882f15cd 100644
--- a/src/prism.c
+++ b/src/prism.c
@@ -14528,6 +14528,50 @@ parse_call_operator_write(pm_parser_t *parser, pm_call_node_t *call_node, const
     }
 }
 
+// Potentially change a =~ with a regular expression with named captures into a
+// match write node.
+static pm_node_t *
+parse_regular_expression_named_captures(pm_parser_t *parser, const pm_string_t *content, pm_call_node_t *call) {
+    pm_string_list_t named_captures;
+    pm_string_list_init(&named_captures);
+
+    pm_node_t *result;
+    if (pm_regexp_named_capture_group_names(pm_string_source(content), pm_string_length(content), &named_captures, parser->encoding_changed, &parser->encoding) && (named_captures.length > 0)) {
+        pm_match_write_node_t *match = pm_match_write_node_create(parser, call);
+
+        for (size_t index = 0; index < named_captures.length; index++) {
+            pm_string_t *name = &named_captures.strings[index];
+            pm_constant_id_t local;
+
+            if (content->type == PM_STRING_SHARED) {
+                // If the unescaped string is a slice of the source,
+                // then we can copy the names directly. The pointers
+                // will line up.
+                local = pm_parser_local_add_location(parser, name->source, name->source + name->length);
+            } else {
+                // Otherwise, the name is a slice of the malloc-ed
+                // owned string, in which case we need to copy it
+                // out into a new string.
+                size_t length = pm_string_length(name);
+
+                void *memory = malloc(length);
+                memcpy(memory, pm_string_source(name), length);
+
+                local = pm_parser_local_add_owned(parser, (const uint8_t *) memory, length);
+            }
+
+            pm_constant_id_list_append(&match->locals, local);
+        }
+
+        result = (pm_node_t *) match;
+    } else {
+        result = (pm_node_t *) call;
+    }
+
+    pm_string_list_free(&named_captures);
+    return result;
+}
+
 static inline pm_node_t *
 parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t previous_binding_power, pm_binding_power_t binding_power) {
     pm_token_t token = parser->current;
@@ -14953,42 +14997,51 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t
             // If the receiver of this =~ is a regular expression node, then we
             // need to introduce local variables for it based on its named
             // capture groups.
-            if (PM_NODE_TYPE_P(node, PM_REGULAR_EXPRESSION_NODE)) {
-                pm_string_list_t named_captures;
-                pm_string_list_init(&named_captures);
-
-                const pm_string_t *unescaped = &((pm_regular_expression_node_t *) node)->unescaped;
-                if (pm_regexp_named_capture_group_names(pm_string_source(unescaped), pm_string_length(unescaped), &named_captures, parser->encoding_changed, &parser->encoding) && (named_captures.length > 0)) {
-                    pm_match_write_node_t *match = pm_match_write_node_create(parser, call);
-
-                    for (size_t index = 0; index < named_captures.length; index++) {
-                        pm_string_t *name = &named_captures.strings[index];
-                        pm_constant_id_t local;
-
-                        if (unescaped->type == PM_STRING_SHARED) {
-                            // If the unescaped string is a slice of the source,
-                            // then we can copy the names directly. The pointers
-                            // will line up.
-                            local = pm_parser_local_add_location(parser, name->source, name->source + name->length);
-                        } else {
-                            // Otherwise, the name is a slice of the malloc-ed
-                            // owned string, in which case we need to copy it
-                            // out into a new string.
-                            size_t length = pm_string_length(name);
+            if (PM_NODE_TYPE_P(node, PM_INTERPOLATED_REGULAR_EXPRESSION_NODE)) {
+                // It's possible to have an interpolated regular expression node
+                // that only contains strings. This is because it can be split
+                // up by a heredoc. In this case we need to concat the unescaped
+                // strings together and then parse them as a regular expression.
+                pm_node_list_t *parts = &((pm_interpolated_regular_expression_node_t *) node)->parts;
 
-                            void *memory = malloc(length);
-                            memcpy(memory, pm_string_source(name), length);
+                bool interpolated = false;
+                size_t total_length = 0;
 
-                            local = pm_parser_local_add_owned(parser, (const uint8_t *) memory, length);
-                        }
+                for (size_t index = 0; index < parts->size; index++) {
+                    pm_node_t *part = parts->nodes[index];
 
-                        pm_constant_id_list_append(&match->locals, local);
+                    if (PM_NODE_TYPE_P(part, PM_STRING_NODE)) {
+                        total_length += pm_string_length(&((pm_string_node_t *) part)->unescaped);
+                    } else {
+                        interpolated = true;
+                        break;
                     }
-
-                    result = (pm_node_t *) match;
                 }
 
-                pm_string_list_free(&named_captures);
+                if (!interpolated) {
+                    void *memory = malloc(total_length);
+                    if (!memory) abort();
+
+                    uint8_t *cursor = memory;
+                    for (size_t index = 0; index < parts->size; index++) {
+                        pm_string_t *unescaped = &((pm_string_node_t *) parts->nodes[index])->unescaped;
+                        size_t length = pm_string_length(unescaped);
+
+                        memcpy(cursor, pm_string_source(unescaped), length);
+                        cursor += length;
+                    }
+
+                    pm_string_t owned;
+                    pm_string_owned_init(&owned, (uint8_t *) memory, total_length);
+
+                    result = parse_regular_expression_named_captures(parser, &owned, call);
+                    pm_string_free(&owned);
+                }
+            } else if (PM_NODE_TYPE_P(node, PM_REGULAR_EXPRESSION_NODE)) {
+                // If we have a regular expression node, then we can just parse
+                // the named captures directly off the unescaped string.
+                const pm_string_t *content = &((pm_regular_expression_node_t *) node)->unescaped;
+                result = parse_regular_expression_named_captures(parser, content, call);
             }
 
             return result;
diff --git a/src/regexp.c b/src/regexp.c
index c227c7b4..3462c846 100644
--- a/src/regexp.c
+++ b/src/regexp.c
@@ -188,6 +188,8 @@ pm_regexp_parse_range_quantifier(pm_regexp_parser_t *parser) {
 //            ;
 static bool
 pm_regexp_parse_quantifier(pm_regexp_parser_t *parser) {
+    if (pm_regexp_char_is_eof(parser)) return true;
+
     switch (*parser->cursor) {
         case '*':
         case '+':


Test patch:
diff --git a/test/prism/fixtures/spanning_heredoc.txt b/test/prism/fixtures/spanning_heredoc.txt
index d88e0e4b..c1b9ec72 100644
--- a/test/prism/fixtures/spanning_heredoc.txt
+++ b/test/prism/fixtures/spanning_heredoc.txt
@@ -49,3 +49,7 @@ pp <<-A, %I[p\
 o
 A
 p]
+
+<<A; /\
+A
+(?<a>)/ =~ ''
diff --git a/test/prism/snapshots/spanning_heredoc.txt b/test/prism/snapshots/spanning_heredoc.txt
index e568dc25..f28aeb81 100644
--- a/test/prism/snapshots/spanning_heredoc.txt
+++ b/test/prism/snapshots/spanning_heredoc.txt
@@ -1,8 +1,8 @@
-@ ProgramNode (location: (4,0)-(51,2))
-├── locals: []
+@ ProgramNode (location: (4,0)-(55,13))
+├── locals: [:a]
 └── statements:
-    @ StatementsNode (location: (4,0)-(51,2))
-    └── body: (length: 8)
+    @ StatementsNode (location: (4,0)-(55,13))
+    └── body: (length: 10)
         ├── @ CallNode (location: (4,0)-(7,7))
         │   ├── receiver: ∅
         │   ├── call_operator_loc: ∅
@@ -270,41 +270,86 @@
         │   ├── block: ∅
         │   ├── flags: ∅
         │   └── name: :pp
-        └── @ CallNode (location: (48,0)-(51,2))
-            ├── receiver: ∅
-            ├── call_operator_loc: ∅
-            ├── message_loc: (48,0)-(48,2) = "pp"
-            ├── opening_loc: ∅
-            ├── arguments:
-            │   @ ArgumentsNode (location: (48,3)-(51,2))
-            │   ├── arguments: (length: 2)
-            │   │   ├── @ StringNode (location: (48,3)-(48,7))
-            │   │   │   ├── flags: ∅
-            │   │   │   ├── opening_loc: (48,3)-(48,7) = "<<-A"
-            │   │   │   ├── content_loc: (49,0)-(50,0) = "o\n"
-            │   │   │   ├── closing_loc: (50,0)-(51,0) = "A\n"
-            │   │   │   └── unescaped: "o\n"
-            │   │   └── @ ArrayNode (location: (48,9)-(51,2))
-            │   │       ├── elements: (length: 1)
-            │   │       │   └── @ InterpolatedSymbolNode (location: (48,12)-(48,14))
-            │   │       │       ├── opening_loc: ∅
-            │   │       │       ├── parts: (length: 2)
-            │   │       │       │   ├── @ SymbolNode (location: (48,12)-(48,14))
-            │   │       │       │   │   ├── opening_loc: ∅
-            │   │       │       │   │   ├── value_loc: (48,12)-(48,14) = "p\\"
-            │   │       │       │   │   ├── closing_loc: ∅
-            │   │       │       │   │   └── unescaped: "p\n"
-            │   │       │       │   └── @ StringNode (location: (48,12)-(48,14))
-            │   │       │       │       ├── flags: ∅
-            │   │       │       │       ├── opening_loc: ∅
-            │   │       │       │       ├── content_loc: (48,12)-(48,14) = "p\\"
-            │   │       │       │       ├── closing_loc: ∅
-            │   │       │       │       └── unescaped: "p"
-            │   │       │       └── closing_loc: ∅
-            │   │       ├── opening_loc: (48,9)-(48,12) = "%I["
-            │   │       └── closing_loc: (51,1)-(51,2) = "]"
-            │   └── flags: ∅
-            ├── closing_loc: ∅
-            ├── block: ∅
-            ├── flags: ∅
-            └── name: :pp
+        ├── @ CallNode (location: (48,0)-(51,2))
+        │   ├── receiver: ∅
+        │   ├── call_operator_loc: ∅
+        │   ├── message_loc: (48,0)-(48,2) = "pp"
+        │   ├── opening_loc: ∅
+        │   ├── arguments:
+        │   │   @ ArgumentsNode (location: (48,3)-(51,2))
+        │   │   ├── arguments: (length: 2)
+        │   │   │   ├── @ StringNode (location: (48,3)-(48,7))
+        │   │   │   │   ├── flags: ∅
+        │   │   │   │   ├── opening_loc: (48,3)-(48,7) = "<<-A"
+        │   │   │   │   ├── content_loc: (49,0)-(50,0) = "o\n"
+        │   │   │   │   ├── closing_loc: (50,0)-(51,0) = "A\n"
+        │   │   │   │   └── unescaped: "o\n"
+        │   │   │   └── @ ArrayNode (location: (48,9)-(51,2))
+        │   │   │       ├── elements: (length: 1)
+        │   │   │       │   └── @ InterpolatedSymbolNode (location: (48,12)-(48,14))
+        │   │   │       │       ├── opening_loc: ∅
+        │   │   │       │       ├── parts: (length: 2)
+        │   │   │       │       │   ├── @ SymbolNode (location: (48,12)-(48,14))
+        │   │   │       │       │   │   ├── opening_loc: ∅
+        │   │   │       │       │   │   ├── value_loc: (48,12)-(48,14) = "p\\"
+        │   │   │       │       │   │   ├── closing_loc: ∅
+        │   │   │       │       │   │   └── unescaped: "p\n"
+        │   │   │       │       │   └── @ StringNode (location: (48,12)-(48,14))
+        │   │   │       │       │       ├── flags: ∅
+        │   │   │       │       │       ├── opening_loc: ∅
+        │   │   │       │       │       ├── content_loc: (48,12)-(48,14) = "p\\"
+        │   │   │       │       │       ├── closing_loc: ∅
+        │   │   │       │       │       └── unescaped: "p"
+        │   │   │       │       └── closing_loc: ∅
+        │   │   │       ├── opening_loc: (48,9)-(48,12) = "%I["
+        │   │   │       └── closing_loc: (51,1)-(51,2) = "]"
+        │   │   └── flags: ∅
+        │   ├── closing_loc: ∅
+        │   ├── block: ∅
+        │   ├── flags: ∅
+        │   └── name: :pp
+        ├── @ StringNode (location: (53,0)-(53,3))
+        │   ├── flags: ∅
+        │   ├── opening_loc: (53,0)-(53,3) = "<<A"
+        │   ├── content_loc: (54,0)-(54,0) = ""
+        │   ├── closing_loc: (54,0)-(55,0) = "A\n"
+        │   └── unescaped: ""
+        └── @ MatchWriteNode (location: (53,5)-(55,13))
+            ├── call:
+            │   @ CallNode (location: (53,5)-(55,13))
+            │   ├── receiver:
+            │   │   @ InterpolatedRegularExpressionNode (location: (53,5)-(55,7))
+            │   │   ├── opening_loc: (53,5)-(53,6) = "/"
+            │   │   ├── parts: (length: 2)
+            │   │   │   ├── @ StringNode (location: (53,6)-(53,7))
+            │   │   │   │   ├── flags: ∅
+            │   │   │   │   ├── opening_loc: ∅
+            │   │   │   │   ├── content_loc: (53,6)-(53,7) = "\\"
+            │   │   │   │   ├── closing_loc: ∅
+            │   │   │   │   └── unescaped: ""
+            │   │   │   └── @ StringNode (location: (55,0)-(55,6))
+            │   │   │       ├── flags: ∅
+            │   │   │       ├── opening_loc: ∅
+            │   │   │       ├── content_loc: (55,0)-(55,6) = "(?<a>)"
+            │   │   │       ├── closing_loc: ∅
+            │   │   │       └── unescaped: "(?<a>)"
+            │   │   ├── closing_loc: (55,6)-(55,7) = "/"
+            │   │   └── flags: ∅
+            │   ├── call_operator_loc: ∅
+            │   ├── message_loc: (55,8)-(55,10) = "=~"
+            │   ├── opening_loc: ∅
+            │   ├── arguments:
+            │   │   @ ArgumentsNode (location: (55,11)-(55,13))
+            │   │   ├── arguments: (length: 1)
+            │   │   │   └── @ StringNode (location: (55,11)-(55,13))
+            │   │   │       ├── flags: ∅
+            │   │   │       ├── opening_loc: (55,11)-(55,12) = "'"
+            │   │   │       ├── content_loc: (55,12)-(55,12) = ""
+            │   │   │       ├── closing_loc: (55,12)-(55,13) = "'"
+            │   │   │       └── unescaped: ""
+            │   │   └── flags: ∅
+            │   ├── closing_loc: ∅
+            │   ├── block: ∅
+            │   ├── flags: ∅
+            │   └── name: :=~
+            └── locals: [:a]