Repository: ruby/prism (c)

Issue:
You can assign to local variables outside the scope with regex
Right now we only have a list of constant IDs for match write nodes, but this doesn't convey the depth. (It actually does add the locals to the correct scope though, so this is more of an enhancement than a bug.)

As an example:

```ruby
a = 1
tap { /(?<a>)/ =~ to_s }
```

This will yield a match write node with `locals: [:a]` with no knowledge of depth. We should replace this `constant[]` with a `node[]` and use local variable target nodes instead.

Pull request description:
Replace match write locals with match write targets
So that we have access to local variable depth. Fixes #1758.

Patch:
diff --git a/bin/encodings b/bin/encodings
index 77de0b75..2a6eb629 100755
--- a/bin/encodings
+++ b/bin/encodings
@@ -63,11 +63,13 @@ when "ibm852"       then table(Encoding::IBM852)
 when "ibm855"       then table(Encoding::IBM855)
 when "ibm857"       then table(Encoding::IBM857)
 when "ibm860"       then table(Encoding::IBM860)
+when "ibm863"       then table(Encoding::IBM863)
 when "ibm861"       then table(Encoding::IBM861)
 when "ibm862"       then table(Encoding::IBM862)
 when "ibm864"       then table(Encoding::IBM864)
 when "ibm865"       then table(Encoding::IBM865)
 when "ibm866"       then table(Encoding::IBM866)
+when "ibm869"       then table(Encoding::IBM869)
 when "iso-8859-1"   then table(Encoding::ISO8859_1)
 when "iso-8859-2"   then table(Encoding::ISO8859_2)
 when "iso-8859-3"   then table(Encoding::ISO8859_3)
diff --git a/config.yml b/config.yml
index 6f12a513..05290173 100644
--- a/config.yml
+++ b/config.yml
@@ -1880,8 +1880,8 @@ nodes:
       - name: call
         type: node
         kind: CallNode
-      - name: locals
-        type: constant[]
+      - name: targets
+        type: node[]
     comment: |
       Represents writing local variables using a regular expression match with
       named capture groups.
diff --git a/docs/encoding.md b/docs/encoding.md
index 4ff8aa9c..9abbcf9c 100644
--- a/docs/encoding.md
+++ b/docs/encoding.md
@@ -34,6 +34,7 @@ The key of the comment can be either "encoding" or "coding". The value of the co
 * `IBM864`
 * `IBM865`
 * `IBM866`
+* `IBM869`
 * `ISO-8859-1`
 * `ISO-8859-2`
 * `ISO-8859-3`
diff --git a/include/prism/enc/pm_encoding.h b/include/prism/enc/pm_encoding.h
index 7279dfd7..1785a842 100644
--- a/include/prism/enc/pm_encoding.h
+++ b/include/prism/enc/pm_encoding.h
@@ -176,9 +176,11 @@ extern pm_encoding_t pm_encoding_ibm857;
 extern pm_encoding_t pm_encoding_ibm860;
 extern pm_encoding_t pm_encoding_ibm861;
 extern pm_encoding_t pm_encoding_ibm862;
+extern pm_encoding_t pm_encoding_ibm863;
 extern pm_encoding_t pm_encoding_ibm864;
 extern pm_encoding_t pm_encoding_ibm865;
 extern pm_encoding_t pm_encoding_ibm866;
+extern pm_encoding_t pm_encoding_ibm869;
 extern pm_encoding_t pm_encoding_iso_8859_1;
 extern pm_encoding_t pm_encoding_iso_8859_2;
 extern pm_encoding_t pm_encoding_iso_8859_3;
diff --git a/src/enc/pm_tables.c b/src/enc/pm_tables.c
index d0414590..70a44720 100644
--- a/src/enc/pm_tables.c
+++ b/src/enc/pm_tables.c
@@ -360,6 +360,30 @@ static uint8_t pm_encoding_ibm862_table[256] = {
     0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Fx
 };
 
+/**
+ * Each element of the following table contains a bitfield that indicates a
+ * piece of information about the corresponding IBM863 character.
+ */
+static uint8_t pm_encoding_ibm863_table[256] = {
+//  0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 2x
+    2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, // 3x
+    0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, // 4x
+    7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 0, 0, 0, 0, 0, // 5x
+    0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, // 6x
+    3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, // 7x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 8x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 9x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Ax
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Bx
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Cx
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Dx
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Ex
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Fx
+};
+
 /**
  * Each element of the following table contains a bitfield that indicates a
  * piece of information about the corresponding IBM864 character.
@@ -432,6 +456,30 @@ static uint8_t pm_encoding_ibm866_table[256] = {
     0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Fx
 };
 
+/**
+ * Each element of the following table contains a bitfield that indicates a
+ * piece of information about the corresponding IBM869 character.
+ */
+static uint8_t pm_encoding_ibm869_table[256] = {
+//  0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 2x
+    2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, // 3x
+    0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, // 4x
+    7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 0, 0, 0, 0, 0, // 5x
+    0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, // 6x
+    3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, // 7x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 8x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 9x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Ax
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Bx
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Cx
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Dx
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Ex
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Fx
+};
+
 /**
  * Each element of the following table contains a bitfield that indicates a
  * piece of information about the corresponding ISO-8859-1 character.
@@ -1375,9 +1423,11 @@ PRISM_ENCODING_TABLE(ibm857)
 PRISM_ENCODING_TABLE(ibm860)
 PRISM_ENCODING_TABLE(ibm861)
 PRISM_ENCODING_TABLE(ibm862)
+PRISM_ENCODING_TABLE(ibm863)
 PRISM_ENCODING_TABLE(ibm864)
 PRISM_ENCODING_TABLE(ibm865)
 PRISM_ENCODING_TABLE(ibm866)
+PRISM_ENCODING_TABLE(ibm869)
 PRISM_ENCODING_TABLE(iso_8859_1)
 PRISM_ENCODING_TABLE(iso_8859_2)
 PRISM_ENCODING_TABLE(iso_8859_3)
@@ -1577,6 +1627,16 @@ pm_encoding_t pm_encoding_ibm862 = {
     .multibyte = false
 };
 
+/** IBM863 */
+pm_encoding_t pm_encoding_ibm863 = {
+    .name = "IBM863",
+    .char_width = pm_encoding_single_char_width,
+    .alnum_char = pm_encoding_ibm863_alnum_char,
+    .alpha_char = pm_encoding_ibm863_alpha_char,
+    .isupper_char = pm_encoding_ibm863_isupper_char,
+    .multibyte = false
+};
+
 /** IBM864 */
 pm_encoding_t pm_encoding_ibm864 = {
     .name = "IBM864",
@@ -1607,6 +1667,16 @@ pm_encoding_t pm_encoding_ibm866 = {
     .multibyte = false
 };
 
+/** IBM869 */
+pm_encoding_t pm_encoding_ibm869 = {
+    .name = "IBM869",
+    .char_width = pm_encoding_single_char_width,
+    .alnum_char = pm_encoding_ibm869_alnum_char,
+    .alpha_char = pm_encoding_ibm869_alpha_char,
+    .isupper_char = pm_encoding_ibm869_isupper_char,
+    .multibyte = false
+};
+
 /** ISO-8859-1 */
 pm_encoding_t pm_encoding_iso_8859_1 = {
     .name = "ISO-8859-1",
diff --git a/src/prism.c b/src/prism.c
index 2607a84c..84a28b93 100644
--- a/src/prism.c
+++ b/src/prism.c
@@ -4028,25 +4028,40 @@ pm_refute_numbered_parameter(pm_parser_t *parser, const uint8_t *start, const ui
 }
 
 /**
- * Allocate and initialize a new LocalVariableTargetNode node.
+ * Allocate and initialize a new LocalVariableTargetNode node with the given
+ * name and depth.
  */
 static pm_local_variable_target_node_t *
-pm_local_variable_target_node_create(pm_parser_t *parser, const pm_token_t *name) {
+pm_local_variable_target_node_create_values(pm_parser_t *parser, const pm_location_t *location, pm_constant_id_t name, uint32_t depth) {
     pm_local_variable_target_node_t *node = PM_ALLOC_NODE(parser, pm_local_variable_target_node_t);
-    pm_refute_numbered_parameter(parser, name->start, name->end);
 
     *node = (pm_local_variable_target_node_t) {
         {
             .type = PM_LOCAL_VARIABLE_TARGET_NODE,
-            .location = PM_LOCATION_TOKEN_VALUE(name)
+            .location = *location
         },
-        .name = pm_parser_constant_id_token(parser, name),
-        .depth = 0
+        .name = name,
+        .depth = depth
     };
 
     return node;
 }
 
+/**
+ * Allocate and initialize a new LocalVariableTargetNode node.
+ */
+static pm_local_variable_target_node_t *
+pm_local_variable_target_node_create(pm_parser_t *parser, const pm_token_t *name) {
+    pm_refute_numbered_parameter(parser, name->start, name->end);
+
+    return pm_local_variable_target_node_create_values(
+        parser,
+        &(pm_location_t) { .start = name->start, .end = name->end },
+        pm_parser_constant_id_token(parser, name),
+        0
+    );
+}
+
 /**
  * Allocate and initialize a new MatchPredicateNode node.
  */
@@ -4109,10 +4124,10 @@ pm_match_write_node_create(pm_parser_t *parser, pm_call_node_t *call) {
             .type = PM_MATCH_WRITE_NODE,
             .location = call->base.location
         },
-        .call = call
+        .call = call,
+        .targets = { 0 }
     };
 
-    pm_constant_id_list_init(&node->locals);
     return node;
 }
 
@@ -5694,17 +5709,16 @@ pm_parser_scope_push_transparent(pm_parser_t *parser) {
 }
 
 /**
- * Check if the current scope has a given local variables.
+ * Check if any of the currently visible scopes contain a local variable
+ * described by the given constant id.
  */
 static int
-pm_parser_local_depth(pm_parser_t *parser, pm_token_t *token) {
-    pm_constant_id_t constant_id = pm_parser_constant_id_token(parser, token);
+pm_parser_local_depth_constant_id(pm_parser_t *parser, pm_constant_id_t constant_id) {
     pm_scope_t *scope = parser->current_scope;
     int depth = 0;
 
     while (scope != NULL) {
-        if (!scope->transparent &&
-                pm_constant_id_list_includes(&scope->locals, constant_id)) return depth;
+        if (!scope->transparent && pm_constant_id_list_includes(&scope->locals, constant_id)) return depth;
         if (scope->closed) break;
 
         scope = scope->previous;
@@ -5714,6 +5728,16 @@ pm_parser_local_depth(pm_parser_t *parser, pm_token_t *token) {
     return -1;
 }
 
+/**
+ * Check if any of the currently visible scopes contain a local variable
+ * described by the given token. This function implicitly inserts a constant
+ * into the constant pool.
+ */
+static inline int
+pm_parser_local_depth(pm_parser_t *parser, pm_token_t *token) {
+    return pm_parser_local_depth_constant_id(parser, pm_parser_constant_id_token(parser, token));
+}
+
 /**
  * Add a constant id to the local table of the current scope.
  */
@@ -6089,8 +6113,10 @@ parser_lex_magic_comment_encoding_value(pm_parser_t *parser, const uint8_t *star
                 ENCODING1("CP864", pm_encoding_ibm864);
                 ENCODING1("CP865", pm_encoding_ibm865);
                 ENCODING1("CP866", pm_encoding_ibm866);
+                ENCODING1("CP869", pm_encoding_ibm869);
                 ENCODING1("CP874", pm_encoding_windows_874);
                 ENCODING1("CP878", pm_encoding_koi8_r);
+                ENCODING1("CP863", pm_encoding_ibm863);
                 ENCODING2("CP932", "csWindows31J", pm_encoding_windows_31j);
                 ENCODING1("CP936", pm_encoding_gbk);
                 ENCODING1("CP1250", pm_encoding_windows_1250);
@@ -6128,9 +6154,11 @@ parser_lex_magic_comment_encoding_value(pm_parser_t *parser, const uint8_t *star
                 ENCODING1("IBM860", pm_encoding_ibm860);
                 ENCODING1("IBM861", pm_encoding_ibm861);
                 ENCODING1("IBM862", pm_encoding_ibm862);
+                ENCODING1("IBM863", pm_encoding_ibm863);
                 ENCODING1("IBM864", pm_encoding_ibm864);
                 ENCODING1("IBM865", pm_encoding_ibm865);
                 ENCODING1("IBM866", pm_encoding_ibm866);
+                ENCODING1("IBM869", pm_encoding_ibm869);
                 ENCODING2("ISO-8859-1", "ISO8859-1", pm_encoding_iso_8859_1);
                 ENCODING2("ISO-8859-2", "ISO8859-2", pm_encoding_iso_8859_2);
                 ENCODING2("ISO-8859-3", "ISO8859-3", pm_encoding_iso_8859_3);
@@ -15762,37 +15790,40 @@ parse_regular_expression_named_captures(pm_parser_t *parser, const pm_string_t *
     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)) {
-        // Since we should not create a MatchWriteNode when all capture names are invalid,
-        // creating a MatchWriteNode is delayed here.
+        // Since we should not create a MatchWriteNode when all capture names
+        // are invalid, creating a MatchWriteNode is delayed here.
         pm_match_write_node_t *match = NULL;
+        pm_constant_id_list_t names = { 0 };
 
         for (size_t index = 0; index < named_captures.length; index++) {
-            pm_string_t *name = &named_captures.strings[index];
+            pm_string_t *string = &named_captures.strings[index];
 
-            const uint8_t *source = pm_string_source(name);
-            size_t length = pm_string_length(name);
+            const uint8_t *source = pm_string_source(string);
+            size_t length = pm_string_length(string);
 
-            if (!name_is_identifier(parser, source, length)) {
-                continue;
-            }
+            pm_location_t location;
+            pm_constant_id_t name;
+
+            // If the name of the capture group isn't a valid identifier, we do
+            // not add it to the local table.
+            if (!name_is_identifier(parser, source, length)) continue;
 
-            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, source, source + length);
+                location = (pm_location_t) { .start = source, .end = source + length };
+                name = pm_parser_constant_id_location(parser, location.start, location.end);
                 pm_refute_numbered_parameter(parser, source, source + 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.
+                location = call->receiver->location;
+
                 void *memory = malloc(length);
                 if (memory == NULL) abort();
 
                 memcpy(memory, source, length);
-
-                // This silences clang analyzer warning about leak of memory pointed by `memory`.
-                // NOLINTNEXTLINE(clang-analyzer-*)
-                local = pm_parser_local_add_owned(parser, (const uint8_t *) memory, length);
+                name = pm_parser_constant_id_owned(parser, (const uint8_t *) memory, length);
 
                 if (pm_token_is_numbered_parameter(source, source + length)) {
                     const pm_location_t *location = &call->receiver->location;
@@ -15800,15 +15831,27 @@ parse_regular_expression_named_captures(pm_parser_t *parser, const pm_string_t *
                 }
             }
 
-            if (match == NULL) {
-                match = pm_match_write_node_create(parser, call);
-            }
+            if (name != 0) {
+                // We dont want to create duplicate targets if the capture name
+                // is duplicated.
+                if (pm_constant_id_list_includes(&names, name)) continue;
+                pm_constant_id_list_append(&names, name);
 
-            if (pm_constant_id_list_includes(&match->locals, local)) {
-                continue;
-            }
+                // Here we lazily create the MatchWriteNode since we know we're
+                // about to add a target.
+                if (match == NULL) match = pm_match_write_node_create(parser, call);
 
-            pm_constant_id_list_append(&match->locals, local);
+                // First, find the depth of the local that is being assigned.
+                int depth;
+                if ((depth = pm_parser_local_depth_constant_id(parser, name)) == -1) {
+                    pm_parser_local_add(parser, name);
+                }
+
+                // Next, create the local variable target and add it to the
+                // list of targets for the match.
+                pm_node_t *target = (pm_node_t *) pm_local_variable_target_node_create_values(parser, &location, name, depth == -1 ? 0 : (uint32_t) depth);
+                pm_node_list_append(&match->targets, target);
+            }
         }
 
         if (match != NULL) {
@@ -15816,6 +15859,8 @@ parse_regular_expression_named_captures(pm_parser_t *parser, const pm_string_t *
         } else {
             result = (pm_node_t *) call;
         }
+
+        pm_constant_id_list_free(&names);
     } else {
         result = (pm_node_t *) call;
     }


Test patch:
diff --git a/test/prism/encoding_test.rb b/test/prism/encoding_test.rb
index a059e227..8704945d 100644
--- a/test/prism/encoding_test.rb
+++ b/test/prism/encoding_test.rb
@@ -23,9 +23,11 @@ module Prism
       Encoding::IBM860 =>       0x00...0x100,
       Encoding::IBM861 =>       0x00...0x100,
       Encoding::IBM862 =>       0x00...0x100,
+      Encoding::IBM863 =>       0x00...0x100,
       Encoding::IBM864 =>       0x00...0x100,
       Encoding::IBM865 =>       0x00...0x100,
       Encoding::IBM866 =>       0x00...0x100,
+      Encoding::IBM869 =>       0x00...0x100,
       Encoding::ISO_8859_1 =>   0x00...0x100,
       Encoding::ISO_8859_2 =>   0x00...0x100,
       Encoding::ISO_8859_3 =>   0x00...0x100,
diff --git a/test/prism/fixtures/regex.txt b/test/prism/fixtures/regex.txt
index c27e022d..ef2f6d45 100644
--- a/test/prism/fixtures/regex.txt
+++ b/test/prism/fixtures/regex.txt
@@ -35,3 +35,6 @@ b>)/ =~ ""; ab
 /(?<abc>)(?<abc>)/ =~ ""; abc
 
 /(?<a b>)/ =~ ""
+
+a = 1
+tap { /(?<a>)/ =~ to_s }
diff --git a/test/prism/snapshots/regex.txt b/test/prism/snapshots/regex.txt
index 722006c9..b69e5cc3 100644
--- a/test/prism/snapshots/regex.txt
+++ b/test/prism/snapshots/regex.txt
@@ -1,8 +1,8 @@
-@ ProgramNode (location: (1,0)-(37,16))
-├── locals: [:foo, :ab, :abc]
+@ ProgramNode (location: (1,0)-(40,24))
+├── locals: [:foo, :ab, :abc, :a]
 └── statements:
-    @ StatementsNode (location: (1,0)-(37,16))
-    └── body: (length: 19)
+    @ StatementsNode (location: (1,0)-(40,24))
+    └── body: (length: 21)
         ├── @ CallNode (location: (1,0)-(1,9))
         │   ├── receiver: ∅
         │   ├── call_operator_loc: ∅
@@ -116,7 +116,10 @@
         │   │   │   │   ├── block: ∅
         │   │   │   │   ├── flags: ∅
         │   │   │   │   └── name: :=~
-        │   │   │   └── locals: [:foo]
+        │   │   │   └── targets: (length: 1)
+        │   │   │       └── @ LocalVariableTargetNode (location: (11,5)-(11,8))
+        │   │   │           ├── name: :foo
+        │   │   │           └── depth: 0
         │   │   └── @ LocalVariableReadNode (location: (11,23)-(11,26))
         │   │       ├── name: :foo
         │   │       └── depth: 0
@@ -237,7 +240,10 @@
         │   │   ├── block: ∅
         │   │   ├── flags: ∅
         │   │   └── name: :=~
-        │   └── locals: [:ab]
+        │   └── targets: (length: 1)
+        │       └── @ LocalVariableTargetNode (location: (32,0)-(33,4))
+        │           ├── name: :ab
+        │           └── depth: 0
         ├── @ LocalVariableReadNode (location: (33,12)-(33,14))
         │   ├── name: :ab
         │   └── depth: 0
@@ -268,32 +274,96 @@
         │   │   ├── block: ∅
         │   │   ├── flags: ∅
         │   │   └── name: :=~
-        │   └── locals: [:abc]
+        │   └── targets: (length: 1)
+        │       └── @ LocalVariableTargetNode (location: (35,4)-(35,7))
+        │           ├── name: :abc
+        │           └── depth: 0
         ├── @ LocalVariableReadNode (location: (35,26)-(35,29))
         │   ├── name: :abc
         │   └── depth: 0
-        └── @ CallNode (location: (37,0)-(37,16))
-            ├── receiver:
-            │   @ RegularExpressionNode (location: (37,0)-(37,10))
-            │   ├── opening_loc: (37,0)-(37,1) = "/"
-            │   ├── content_loc: (37,1)-(37,9) = "(?<a b>)"
-            │   ├── closing_loc: (37,9)-(37,10) = "/"
-            │   ├── unescaped: "(?<a b>)"
-            │   └── flags: ∅
+        ├── @ CallNode (location: (37,0)-(37,16))
+        │   ├── receiver:
+        │   │   @ RegularExpressionNode (location: (37,0)-(37,10))
+        │   │   ├── opening_loc: (37,0)-(37,1) = "/"
+        │   │   ├── content_loc: (37,1)-(37,9) = "(?<a b>)"
+        │   │   ├── closing_loc: (37,9)-(37,10) = "/"
+        │   │   ├── unescaped: "(?<a b>)"
+        │   │   └── flags: ∅
+        │   ├── call_operator_loc: ∅
+        │   ├── message_loc: (37,11)-(37,13) = "=~"
+        │   ├── opening_loc: ∅
+        │   ├── arguments:
+        │   │   @ ArgumentsNode (location: (37,14)-(37,16))
+        │   │   ├── arguments: (length: 1)
+        │   │   │   └── @ StringNode (location: (37,14)-(37,16))
+        │   │   │       ├── flags: ∅
+        │   │   │       ├── opening_loc: (37,14)-(37,15) = "\""
+        │   │   │       ├── content_loc: (37,15)-(37,15) = ""
+        │   │   │       ├── closing_loc: (37,15)-(37,16) = "\""
+        │   │   │       └── unescaped: ""
+        │   │   └── flags: ∅
+        │   ├── closing_loc: ∅
+        │   ├── block: ∅
+        │   ├── flags: ∅
+        │   └── name: :=~
+        ├── @ LocalVariableWriteNode (location: (39,0)-(39,5))
+        │   ├── name: :a
+        │   ├── depth: 0
+        │   ├── name_loc: (39,0)-(39,1) = "a"
+        │   ├── value:
+        │   │   @ IntegerNode (location: (39,4)-(39,5))
+        │   │   └── flags: decimal
+        │   └── operator_loc: (39,2)-(39,3) = "="
+        └── @ CallNode (location: (40,0)-(40,24))
+            ├── receiver: ∅
             ├── call_operator_loc: ∅
-            ├── message_loc: (37,11)-(37,13) = "=~"
+            ├── message_loc: (40,0)-(40,3) = "tap"
             ├── opening_loc: ∅
-            ├── arguments:
-            │   @ ArgumentsNode (location: (37,14)-(37,16))
-            │   ├── arguments: (length: 1)
-            │   │   └── @ StringNode (location: (37,14)-(37,16))
-            │   │       ├── flags: ∅
-            │   │       ├── opening_loc: (37,14)-(37,15) = "\""
-            │   │       ├── content_loc: (37,15)-(37,15) = ""
-            │   │       ├── closing_loc: (37,15)-(37,16) = "\""
-            │   │       └── unescaped: ""
-            │   └── flags: ∅
+            ├── arguments: ∅
             ├── closing_loc: ∅
-            ├── block: ∅
+            ├── block:
+            │   @ BlockNode (location: (40,4)-(40,24))
+            │   ├── locals: []
+            │   ├── parameters: ∅
+            │   ├── body:
+            │   │   @ StatementsNode (location: (40,6)-(40,22))
+            │   │   └── body: (length: 1)
+            │   │       └── @ MatchWriteNode (location: (40,6)-(40,22))
+            │   │           ├── call:
+            │   │           │   @ CallNode (location: (40,6)-(40,22))
+            │   │           │   ├── receiver:
+            │   │           │   │   @ RegularExpressionNode (location: (40,6)-(40,14))
+            │   │           │   │   ├── opening_loc: (40,6)-(40,7) = "/"
+            │   │           │   │   ├── content_loc: (40,7)-(40,13) = "(?<a>)"
+            │   │           │   │   ├── closing_loc: (40,13)-(40,14) = "/"
+            │   │           │   │   ├── unescaped: "(?<a>)"
+            │   │           │   │   └── flags: ∅
+            │   │           │   ├── call_operator_loc: ∅
+            │   │           │   ├── message_loc: (40,15)-(40,17) = "=~"
+            │   │           │   ├── opening_loc: ∅
+            │   │           │   ├── arguments:
+            │   │           │   │   @ ArgumentsNode (location: (40,18)-(40,22))
+            │   │           │   │   ├── arguments: (length: 1)
+            │   │           │   │   │   └── @ CallNode (location: (40,18)-(40,22))
+            │   │           │   │   │       ├── receiver: ∅
+            │   │           │   │   │       ├── call_operator_loc: ∅
+            │   │           │   │   │       ├── message_loc: (40,18)-(40,22) = "to_s"
+            │   │           │   │   │       ├── opening_loc: ∅
+            │   │           │   │   │       ├── arguments: ∅
+            │   │           │   │   │       ├── closing_loc: ∅
+            │   │           │   │   │       ├── block: ∅
+            │   │           │   │   │       ├── flags: variable_call
+            │   │           │   │   │       └── name: :to_s
+            │   │           │   │   └── flags: ∅
+            │   │           │   ├── closing_loc: ∅
+            │   │           │   ├── block: ∅
+            │   │           │   ├── flags: ∅
+            │   │           │   └── name: :=~
+            │   │           └── targets: (length: 1)
+            │   │               └── @ LocalVariableTargetNode (location: (40,10)-(40,11))
+            │   │                   ├── name: :a
+            │   │                   └── depth: 1
+            │   ├── opening_loc: (40,4)-(40,5) = "{"
+            │   └── closing_loc: (40,23)-(40,24) = "}"
             ├── flags: ∅
-            └── name: :=~
+            └── name: :tap
diff --git a/test/prism/snapshots/spanning_heredoc.txt b/test/prism/snapshots/spanning_heredoc.txt
index f28aeb81..ed9c03e6 100644
--- a/test/prism/snapshots/spanning_heredoc.txt
+++ b/test/prism/snapshots/spanning_heredoc.txt
@@ -352,4 +352,7 @@
             │   ├── block: ∅
             │   ├── flags: ∅
             │   └── name: :=~
-            └── locals: [:a]
+            └── targets: (length: 1)
+                └── @ LocalVariableTargetNode (location: (53,5)-(55,7))
+                    ├── name: :a
+                    └── depth: 0
diff --git a/test/prism/snapshots/whitequark/lvar_injecting_match.txt b/test/prism/snapshots/whitequark/lvar_injecting_match.txt
index 0d615cc1..de6d5eb9 100644
--- a/test/prism/snapshots/whitequark/lvar_injecting_match.txt
+++ b/test/prism/snapshots/whitequark/lvar_injecting_match.txt
@@ -30,7 +30,10 @@
         │   │   ├── block: ∅
         │   │   ├── flags: ∅
         │   │   └── name: :=~
-        │   └── locals: [:match]
+        │   └── targets: (length: 1)
+        │       └── @ LocalVariableTargetNode (location: (1,4)-(1,9))
+        │           ├── name: :match
+        │           └── depth: 0
         └── @ LocalVariableReadNode (location: (1,26)-(1,31))
             ├── name: :match
             └── depth: 0