Repository: ruby/yarp
Pull request #1449 (2023-09-12 14:11:53)

[rust] fix accessing `ConstantId` slices
When I wrote `ConstantId::as_slice`, I misunderstood how `yp_constant_id_t` works in concert with `yp_constant_pool_t`: it's not a straight index into the pool of constants.

This implementation is, of course, suboptimal in the sense that we're taking what should be a cheap operation and making it linear in the number of constant ids the program contains, but it is at least correct.  I don't have a test for this, as the example that I was seeing this on doesn't fail as a standalone test in the testsuite, for reasons I don't fully understand.

It would not be entirely surprising to me if everybody using the C API gets bitten by this.  Happy to rewrite the constant pool implementation so it works more like one would expect.


Patch:
diff --git a/rust/yarp/build.rs b/rust/yarp/build.rs
index 7944bdfd..0b739c46 100644
--- a/rust/yarp/build.rs
+++ b/rust/yarp/build.rs
@@ -519,12 +519,21 @@ impl<'pr> ConstantId<'pr> {{
     }}
 
     /// Returns a byte slice for the constant ID.
+    ///
+    /// # Panics
+    ///
+    /// Panics if the constant ID is not found in the constant pool.
     #[must_use]
     pub fn as_slice(&self) -> &'pr [u8] {{
         unsafe {{
             let pool = &(*self.parser.as_ptr()).constant_pool;
-            let constant = &(*pool.constants.offset(isize::try_from(self.id).expect("id should be in range")));
-            std::slice::from_raw_parts(constant.start, constant.length)
+            for i in 0..pool.capacity {{
+                let constant = &(*pool.constants.add(i));
+                if constant.id() == self.id {{
+                    return std::slice::from_raw_parts(constant.start, constant.length);
+                }}
+            }}
+            panic!("Unable to locate constant id");
         }}
     }}
 }}
diff --git a/src/yarp.c b/src/yarp.c
index 28d71f69..730bf827 100644
--- a/src/yarp.c
+++ b/src/yarp.c
@@ -10037,53 +10037,6 @@ parse_method_definition_name(yp_parser_t *parser) {
     }
 }
 
-static int
-parse_heredoc_common_whitespace_for_single_node(yp_parser_t *parser, yp_node_t *node, int common_whitespace)
-{
-    const yp_location_t *content_loc = &((yp_string_node_t *) node)->content_loc;
-    int cur_whitespace;
-    const uint8_t *cur_char = content_loc->start;
-
-    while (cur_char && cur_char < content_loc->end) {
-        // Any empty newlines aren't included in the minimum whitespace
-        // calculation.
-        size_t eol_length;
-        while ((eol_length = match_eol_at(parser, cur_char))) {
-            cur_char += eol_length;
-        }
-
-        if (cur_char == content_loc->end) break;
-
-        cur_whitespace = 0;
-
-        while (yp_char_is_inline_whitespace(*cur_char) && cur_char < content_loc->end) {
-            if (cur_char[0] == '\t') {
-                cur_whitespace = (cur_whitespace / YP_TAB_WHITESPACE_SIZE + 1) * YP_TAB_WHITESPACE_SIZE;
-            } else {
-                cur_whitespace++;
-            }
-            cur_char++;
-        }
-
-        // If we hit a newline, then we have encountered a line that
-        // contains only whitespace, and it shouldn't be considered in
-        // the calculation of common leading whitespace.
-        eol_length = match_eol_at(parser, cur_char);
-        if (eol_length) {
-            cur_char += eol_length;
-            continue;
-        }
-
-        if (cur_whitespace < common_whitespace || common_whitespace == -1) {
-            common_whitespace = cur_whitespace;
-        }
-
-        cur_char = next_newline(cur_char + 1, parser->end - (cur_char + 1));
-        if (cur_char) cur_char++;
-    }
-    return common_whitespace;
-}
-
 // Calculate the common leading whitespace for each line in a heredoc.
 static int
 parse_heredoc_common_whitespace(yp_parser_t *parser, yp_node_list_t *nodes) {
@@ -10091,102 +10044,69 @@ parse_heredoc_common_whitespace(yp_parser_t *parser, yp_node_list_t *nodes) {
 
     for (size_t index = 0; index < nodes->size; index++) {
         yp_node_t *node = nodes->nodes[index];
+
         if (!YP_NODE_TYPE_P(node, YP_STRING_NODE)) continue;
+        const yp_location_t *content_loc = &((yp_string_node_t *) node)->content_loc;
 
         // If the previous node wasn't a string node, we don't want to trim
         // whitespace. This could happen after an interpolated expression or
         // variable.
         if (index == 0 || YP_NODE_TYPE_P(nodes->nodes[index - 1], YP_STRING_NODE)) {
-            common_whitespace = parse_heredoc_common_whitespace_for_single_node(parser, node, common_whitespace);
-        }
-    }
-
-    return common_whitespace;
-}
+            int cur_whitespace;
+            const uint8_t *cur_char = content_loc->start;
 
-static yp_string_t *
-parse_heredoc_dedent_single_node(yp_parser_t *parser, yp_string_t *string, bool dedent_node, int common_whitespace, yp_heredoc_quote_t quote)
-{
-    // Get a reference to the string struct that is being held by the string
-    // node. This is the value we're going to actually manipulate.
-    yp_string_ensure_owned(string);
-
-    // Now get the bounds of the existing string. We'll use this as a
-    // destination to move bytes into. We'll also use it for bounds checking
-    // since we don't require that these strings be null terminated.
-    size_t dest_length = yp_string_length(string);
-    uint8_t *source_start = (uint8_t *) string->source;
-
-    const uint8_t *source_cursor = source_start;
-    const uint8_t *source_end = source_cursor + dest_length;
-
-    // We're going to move bytes backward in the string when we get leading
-    // whitespace, so we'll maintain a pointer to the current position in the
-    // string that we're writing to.
-    uint8_t *dest_cursor = source_start;
-
-    while (source_cursor < source_end) {
-        // If we need to dedent the next element within the heredoc or the next
-        // line within the string node, then we'll do it here.
-        if (dedent_node) {
-            int trimmed_whitespace = 0;
-
-            // While we haven't reached the amount of common whitespace that we need
-            // to trim and we haven't reached the end of the string, we'll keep
-            // trimming whitespace. Trimming in this context means skipping over
-            // these bytes such that they aren't copied into the new string.
-            while ((source_cursor < source_end) && yp_char_is_inline_whitespace(*source_cursor) && trimmed_whitespace < common_whitespace) {
-                if (*source_cursor == '\t') {
-                    trimmed_whitespace = (trimmed_whitespace / YP_TAB_WHITESPACE_SIZE + 1) * YP_TAB_WHITESPACE_SIZE;
-                    if (trimmed_whitespace > common_whitespace) break;
-                } else {
-                    trimmed_whitespace++;
+            while (cur_char && cur_char < content_loc->end) {
+                // Any empty newlines aren't included in the minimum whitespace
+                // calculation.
+                size_t eol_length;
+                while ((eol_length = match_eol_at(parser, cur_char))) {
+                    cur_char += eol_length;
                 }
 
-                source_cursor++;
-                dest_length--;
-            }
-        }
+                if (cur_char == content_loc->end) break;
 
-        // At this point we have dedented all that we need to, so we need to find
-        // the next newline.
-        const uint8_t *breakpoint = next_newline(source_cursor, source_end - source_cursor);
+                cur_whitespace = 0;
 
-        if (breakpoint == NULL) {
-            // If there isn't another newline, then we can just move the rest of the
-            // string and break from the loop.
-            memmove(dest_cursor, source_cursor, (size_t) (source_end - source_cursor));
-            break;
-        }
+                while (yp_char_is_inline_whitespace(*cur_char) && cur_char < content_loc->end) {
+                    if (cur_char[0] == '\t') {
+                        cur_whitespace = (cur_whitespace / YP_TAB_WHITESPACE_SIZE + 1) * YP_TAB_WHITESPACE_SIZE;
+                    } else {
+                        cur_whitespace++;
+                    }
+                    cur_char++;
+                }
 
-        // Otherwise, we need to move everything including the newline, and
-        // then set the dedent_node flag to true.
-        if (breakpoint < source_end) breakpoint++;
-        memmove(dest_cursor, source_cursor, (size_t) (breakpoint - source_cursor));
-        dest_cursor += (breakpoint - source_cursor);
-        source_cursor = breakpoint;
-        dedent_node = true;
-    }
+                // If we hit a newline, then we have encountered a line that
+                // contains only whitespace, and it shouldn't be considered in
+                // the calculation of common leading whitespace.
+                eol_length = match_eol_at(parser, cur_char);
+                if (eol_length) {
+                    cur_char += eol_length;
+                    continue;
+                }
 
-    // We only want to write this node into the list if it has any content.
-    string->length = dest_length;
+                if (cur_whitespace < common_whitespace || common_whitespace == -1) {
+                    common_whitespace = cur_whitespace;
+                }
 
-    if (dest_length != 0) {
-        yp_unescape_manipulate_string(parser, string, (quote == YP_HEREDOC_QUOTE_SINGLE) ? YP_UNESCAPE_MINIMAL : YP_UNESCAPE_ALL);
+                cur_char = next_newline(cur_char + 1, parser->end - (cur_char + 1));
+                if (cur_char) cur_char++;
+            }
+        }
     }
-    return string;
+
+    return common_whitespace;
 }
 
 // Take a heredoc node that is indented by a ~ and trim the leading whitespace.
 static void
-parse_heredoc_dedent(yp_parser_t *parser, yp_node_t *heredoc_node, yp_heredoc_quote_t quote)
-{
+parse_heredoc_dedent(yp_parser_t *parser, yp_node_t *node, yp_heredoc_quote_t quote) {
     yp_node_list_t *nodes;
 
     if (quote == YP_HEREDOC_QUOTE_BACKTICK) {
-        nodes = &((yp_interpolated_x_string_node_t *) heredoc_node)->parts;
+        nodes = &((yp_interpolated_x_string_node_t *) node)->parts;
     } else {
-        nodes = &((yp_interpolated_string_node_t *) heredoc_node)->parts;
+        nodes = &((yp_interpolated_string_node_t *) node)->parts;
     }
 
     // First, calculate how much common whitespace we need to trim. If there is
@@ -10215,11 +10135,74 @@ parse_heredoc_dedent(yp_parser_t *parser, yp_node_t *heredoc_node, yp_heredoc_qu
             continue;
         }
 
-        yp_string_node_t *string_node = ((yp_string_node_t *) node);
-        parse_heredoc_dedent_single_node(parser, &string_node->unescaped, dedent_next, common_whitespace, quote);
-        if (string_node->unescaped.length == 0) {
+        // Get a reference to the string struct that is being held by the string
+        // node. This is the value we're going to actual manipulate.
+        yp_string_t *string = &(((yp_string_node_t *) node)->unescaped);
+        yp_string_ensure_owned(string);
+
+        // Now get the bounds of the existing string. We'll use this as a
+        // destination to move bytes into. We'll also use it for bounds checking
+        // since we don't require that these strings be null terminated.
+        size_t dest_length = yp_string_length(string);
+        uint8_t *source_start = (uint8_t *) string->source;
+
+        const uint8_t *source_cursor = source_start;
+        const uint8_t *source_end = source_cursor + dest_length;
+
+        // We're going to move bytes backward in the string when we get leading
+        // whitespace, so we'll maintain a pointer to the current position in the
+        // string that we're writing to.
+        uint8_t *dest_cursor = source_start;
+
+        while (source_cursor < source_end) {
+            // If we need to dedent the next element within the heredoc or the next
+            // line within the string node, then we'll do it here.
+            if (dedent_next) {
+                int trimmed_whitespace = 0;
+
+                // While we haven't reached the amount of common whitespace that we need
+                // to trim and we haven't reached the end of the string, we'll keep
+                // trimming whitespace. Trimming in this context means skipping over
+                // these bytes such that they aren't copied into the new string.
+                while ((source_cursor < source_end) && yp_char_is_inline_whitespace(*source_cursor) && trimmed_whitespace < common_whitespace) {
+                    if (*source_cursor == '\t') {
+                        trimmed_whitespace = (trimmed_whitespace / YP_TAB_WHITESPACE_SIZE + 1) * YP_TAB_WHITESPACE_SIZE;
+                        if (trimmed_whitespace > common_whitespace) break;
+                    } else {
+                        trimmed_whitespace++;
+                    }
+
+                    source_cursor++;
+                    dest_length--;
+                }
+            }
+
+            // At this point we have dedented all that we need to, so we need to find
+            // the next newline.
+            const uint8_t *breakpoint = next_newline(source_cursor, source_end - source_cursor);
+
+            if (breakpoint == NULL) {
+                // If there isn't another newline, then we can just move the rest of the
+                // string and break from the loop.
+                memmove(dest_cursor, source_cursor, (size_t) (source_end - source_cursor));
+                break;
+            }
+
+            // Otherwise, we need to move everything including the newline, and
+            // then set the dedent_next flag to true.
+            if (breakpoint < source_end) breakpoint++;
+            memmove(dest_cursor, source_cursor, (size_t) (breakpoint - source_cursor));
+            dest_cursor += (breakpoint - source_cursor);
+            source_cursor = breakpoint;
+            dedent_next = true;
+        }
+
+        // We only want to write this node into the list if it has any content.
+        if (dest_length == 0) {
             yp_node_destroy(parser, node);
         } else {
+            string->length = dest_length;
+            yp_unescape_manipulate_string(parser, string, (quote == YP_HEREDOC_QUOTE_SINGLE) ? YP_UNESCAPE_MINIMAL : YP_UNESCAPE_ALL);
             nodes->nodes[write_index++] = node;
         }
 
@@ -11263,94 +11246,42 @@ parse_expression_prefix(yp_parser_t *parser, yp_binding_power_t binding_power) {
             yp_heredoc_indent_t indent = parser->lex_modes.current->as.heredoc.indent;
 
             yp_node_t *node;
-
-            parser_lex(parser);
-
-            if (parser->current.type == YP_TOKEN_HEREDOC_END) {
-                if (quote == YP_HEREDOC_QUOTE_BACKTICK) {
-                    node = (yp_node_t *) yp_xstring_node_create(
-                            parser,
-                            &parser->previous,
-                            &((yp_token_t) { .type = YP_TOKEN_STRING_CONTENT, .start = parser->previous.end, .end = parser->current.start }),
-                            &parser->current);
-
-                } else {
-                    node = (yp_node_t *)yp_string_node_create(
-                            parser,
-                            &parser->previous,
-                            &((yp_token_t) { .type = YP_TOKEN_STRING_CONTENT, .start = parser->previous.end, .end = parser->current.start }),
-                            &parser->current);
-                }
-                node->location.end = parser->previous.end;
-                lex_state_set(parser, YP_LEX_STATE_END);
-                expect(parser, YP_TOKEN_HEREDOC_END, YP_ERR_HEREDOC_TERM);
-                return node;
-            }
-
-            yp_token_t opening_token = parser->previous;
-
-            yp_node_t *part = parse_string_part(parser);
-
-            if (part == NULL) {
-                // We couldn't parse anything, so return a missing node
-                return (yp_node_t *) yp_missing_node_create(parser, parser->previous.start, parser->previous.end);
+            if (quote == YP_HEREDOC_QUOTE_BACKTICK) {
+                node = (yp_node_t *) yp_interpolated_xstring_node_create(parser, &parser->current, &parser->current);
+            } else {
+                node = (yp_node_t *) yp_interpolated_string_node_create(parser, &parser->current, NULL, &parser->current);
             }
 
-            if (YP_NODE_TYPE_P(part, YP_STRING_NODE) && match_any_type_p(parser, 2, YP_TOKEN_HEREDOC_END, YP_TOKEN_EOF)) {
-                // We only have a single string, so we can return it
-                yp_string_node_t *str_part = (yp_string_node_t *)part;
-                str_part->opening_loc = YP_LOCATION_TOKEN_VALUE(&opening_token);
-                str_part->closing_loc = YP_LOCATION_TOKEN_VALUE(&parser->current);
-                str_part->base.location = str_part->opening_loc;
-                if (quote == YP_HEREDOC_QUOTE_BACKTICK) {
-                    part->type = YP_X_STRING_NODE;
-                }
-                lex_state_set(parser, YP_LEX_STATE_END);
-                expect(parser, YP_TOKEN_HEREDOC_END, YP_ERR_HEREDOC_TERM);
-                node = part;
-                if (indent == YP_HEREDOC_INDENT_TILDE) {
-                    int common_whitespace = parse_heredoc_common_whitespace_for_single_node(parser, node, -1);
-                    parse_heredoc_dedent_single_node(parser, &str_part->unescaped, true, common_whitespace, quote);
-                }
-            }
-            else {
-                // We have multiple parts, continue parsing them
-                yp_node_list_t parts = YP_EMPTY_NODE_LIST;
-                yp_node_list_append(&parts, part);
+            parser_lex(parser);
+            yp_node_t *part;
 
-                while (!match_any_type_p(parser, 2, YP_TOKEN_HEREDOC_END, YP_TOKEN_EOF)) {
-                    if ((part = parse_string_part(parser)) != NULL) {
-                        yp_node_list_append(&parts, part);
-                    }
-                }
+            while (!match_any_type_p(parser, 2, YP_TOKEN_HEREDOC_END, YP_TOKEN_EOF)) {
+                if ((part = parse_string_part(parser)) == NULL) continue;
 
                 if (quote == YP_HEREDOC_QUOTE_BACKTICK) {
-                    node = (yp_node_t *) yp_interpolated_xstring_node_create(parser, &opening_token, &opening_token);
-                    ((yp_interpolated_x_string_node_t *)node)->parts = parts;
+                    yp_interpolated_xstring_node_append((yp_interpolated_x_string_node_t *) node, part);
                 } else {
-                    node = (yp_node_t *) yp_interpolated_string_node_create(parser, &opening_token, NULL, &opening_token);
-                    ((yp_interpolated_string_node_t *)node)->parts = parts;
+                    yp_interpolated_string_node_append((yp_interpolated_string_node_t *) node, part);
                 }
+            }
 
+            lex_state_set(parser, YP_LEX_STATE_END);
+            expect(parser, YP_TOKEN_HEREDOC_END, YP_ERR_HEREDOC_TERM);
 
-                lex_state_set(parser, YP_LEX_STATE_END);
-                expect(parser, YP_TOKEN_HEREDOC_END, YP_ERR_HEREDOC_TERM);
-
-                if (quote == YP_HEREDOC_QUOTE_BACKTICK) {
-                    assert(YP_NODE_TYPE_P(node, YP_INTERPOLATED_X_STRING_NODE));
-                    yp_interpolated_xstring_node_closing_set(((yp_interpolated_x_string_node_t *) node), &parser->previous);
-                    node->location = ((yp_interpolated_x_string_node_t *) node)->opening_loc;
-                } else {
-                    assert(YP_NODE_TYPE_P(node, YP_INTERPOLATED_STRING_NODE));
-                    yp_interpolated_string_node_closing_set((yp_interpolated_string_node_t *) node, &parser->previous);
-                    node->location = ((yp_interpolated_string_node_t *) node)->opening_loc;
-                }
+            if (quote == YP_HEREDOC_QUOTE_BACKTICK) {
+                assert(YP_NODE_TYPE_P(node, YP_INTERPOLATED_X_STRING_NODE));
+                yp_interpolated_xstring_node_closing_set(((yp_interpolated_x_string_node_t *) node), &parser->previous);
+                node->location = ((yp_interpolated_x_string_node_t *) node)->opening_loc;
+            } else {
+                assert(YP_NODE_TYPE_P(node, YP_INTERPOLATED_STRING_NODE));
+                yp_interpolated_string_node_closing_set((yp_interpolated_string_node_t *) node, &parser->previous);
+                node->location = ((yp_interpolated_string_node_t *) node)->opening_loc;
+            }
 
-                // If this is a heredoc that is indented with a ~, then we need to dedent
-                // each line by the common leading whitespace.
-                if (indent == YP_HEREDOC_INDENT_TILDE) {
-                    parse_heredoc_dedent(parser, node, quote);
-                }
+            // If this is a heredoc that is indented with a ~, then we need to dedent
+            // each line by the common leading whitespace.
+            if (indent == YP_HEREDOC_INDENT_TILDE) {
+                parse_heredoc_dedent(parser, node, quote);
             }
 
             // If there's a string immediately following this heredoc, then it's a


Test patch:
diff --git a/test/yarp/heredoc_dedent_test.rb b/test/yarp/heredoc_dedent_test.rb
index 9d26febc..97523288 100644
--- a/test/yarp/heredoc_dedent_test.rb
+++ b/test/yarp/heredoc_dedent_test.rb
@@ -8,12 +8,8 @@ module YARP
 
     File.read(filepath).split(/(?=\n)\n(?=<)/).each_with_index do |heredoc, index|
       define_method "test_heredoc_#{index}" do
-        node = YARP.parse(heredoc).value.statements.body.first
-        if node.is_a? StringNode
-          actual = node.unescaped
-        else
-          actual = node.parts.map { |part| part.is_a?(StringNode) ? part.unescaped : "1" }.join
-        end
+        parts = YARP.parse(heredoc).value.statements.body.first.parts
+        actual = parts.map { |part| part.is_a?(StringNode) ? part.unescaped : "1" }.join
 
         assert_equal(eval(heredoc), actual, "Expected heredocs to match.")
       end
diff --git a/test/yarp/location_test.rb b/test/yarp/location_test.rb
index 253d6160..46cae55c 100644
--- a/test/yarp/location_test.rb
+++ b/test/yarp/location_test.rb
@@ -460,7 +460,7 @@ module YARP
 
     def test_InterpolatedStringNode
       assert_location(InterpolatedStringNode, "\"foo \#@bar baz\"")
-      assert_location(InterpolatedStringNode, "<<~A\nhello \#{1} world\nA", 0...4)
+      assert_location(InterpolatedStringNode, "<<~A\nhello world\nA", 0...4)
     end
 
     def test_InterpolatedSymbolNode
diff --git a/test/yarp/parse_test.rb b/test/yarp/parse_test.rb
index 1325bc03..5299cfd7 100644
--- a/test/yarp/parse_test.rb
+++ b/test/yarp/parse_test.rb
@@ -186,11 +186,7 @@ module YARP
         # We only want to compare parent/child location overlap in the case that
         # we are not looking at a heredoc. That's because heredoc locations are
         # special in that they only use the declaration of the heredoc.
-        compare = !(current.is_a?(StringNode) ||
-                    current.is_a?(XStringNode) ||
-                    current.is_a?(InterpolatedStringNode) ||
-                    current.is_a?(InterpolatedXStringNode)) ||
-        !current.opening&.start_with?("<<")
+        compare = !(current.is_a?(InterpolatedStringNode) || current.is_a?(InterpolatedXStringNode)) || !current.opening&.start_with?("<<")
 
         current.child_nodes.each do |child|
           # child_nodes can return nil values, so we need to skip those.
diff --git a/test/yarp/snapshots/dash_heredocs.txt b/test/yarp/snapshots/dash_heredocs.txt
index 1e53e000..28537bf2 100644
--- a/test/yarp/snapshots/dash_heredocs.txt
+++ b/test/yarp/snapshots/dash_heredocs.txt
@@ -3,29 +3,41 @@
 └── statements:
     @ StatementsNode (location: (0...278))
     └── body: (length: 13)
-        ├── @ StringNode (location: (0...6))
+        ├── @ InterpolatedStringNode (location: (0...6))
         │   ├── opening_loc: (0...6) = "<<-EOF"
-        │   ├── content_loc: (7...11) = "  a\n"
-        │   ├── closing_loc: (11...15) = "EOF\n"
-        │   └── unescaped: "  a\n"
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (7...11))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (7...11) = "  a\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "  a\n"
+        │   └── closing_loc: (11...15) = "EOF\n"
         ├── @ CallNode (location: (16...36))
         │   ├── receiver:
-        │   │   @ StringNode (location: (16...24))
+        │   │   @ InterpolatedStringNode (location: (16...24))
         │   │   ├── opening_loc: (16...24) = "<<-FIRST"
-        │   │   ├── content_loc: (37...41) = "  a\n"
-        │   │   ├── closing_loc: (41...47) = "FIRST\n"
-        │   │   └── unescaped: "  a\n"
+        │   │   ├── parts: (length: 1)
+        │   │   │   └── @ StringNode (location: (37...41))
+        │   │   │       ├── opening_loc: ∅
+        │   │   │       ├── content_loc: (37...41) = "  a\n"
+        │   │   │       ├── closing_loc: ∅
+        │   │   │       └── unescaped: "  a\n"
+        │   │   └── closing_loc: (41...47) = "FIRST\n"
         │   ├── call_operator_loc: ∅
         │   ├── message_loc: (25...26) = "+"
         │   ├── opening_loc: ∅
         │   ├── arguments:
         │   │   @ ArgumentsNode (location: (27...36))
         │   │   └── arguments: (length: 1)
-        │   │       └── @ StringNode (location: (27...36))
+        │   │       └── @ InterpolatedStringNode (location: (27...36))
         │   │           ├── opening_loc: (27...36) = "<<-SECOND"
-        │   │           ├── content_loc: (47...51) = "  b\n"
-        │   │           ├── closing_loc: (51...58) = "SECOND\n"
-        │   │           └── unescaped: "  b\n"
+        │   │           ├── parts: (length: 1)
+        │   │           │   └── @ StringNode (location: (47...51))
+        │   │           │       ├── opening_loc: ∅
+        │   │           │       ├── content_loc: (47...51) = "  b\n"
+        │   │           │       ├── closing_loc: ∅
+        │   │           │       └── unescaped: "  b\n"
+        │   │           └── closing_loc: (51...58) = "SECOND\n"
         │   ├── closing_loc: ∅
         │   ├── block: ∅
         │   ├── flags: 
@@ -60,16 +72,24 @@
         │   │       ├── closing_loc: ∅
         │   │       └── unescaped: "\n"
         │   └── closing_loc: (77...81) = "EOF\n"
-        ├── @ StringNode (location: (82...88))
+        ├── @ InterpolatedStringNode (location: (82...88))
         │   ├── opening_loc: (82...88) = "<<-EOF"
-        │   ├── content_loc: (98...102) = "  a\n"
-        │   ├── closing_loc: (102...106) = "EOF\n"
-        │   └── unescaped: "  a\n"
-        ├── @ StringNode (location: (107...113))
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (98...102))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (98...102) = "  a\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "  a\n"
+        │   └── closing_loc: (102...106) = "EOF\n"
+        ├── @ InterpolatedStringNode (location: (107...113))
         │   ├── opening_loc: (107...113) = "<<-EOF"
-        │   ├── content_loc: (114...122) = "  a\n  b\n"
-        │   ├── closing_loc: (122...128) = "  EOF\n"
-        │   └── unescaped: "  a\n  b\n"
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (114...122))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (114...122) = "  a\n  b\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "  a\n  b\n"
+        │   └── closing_loc: (122...128) = "  EOF\n"
         ├── @ InterpolatedStringNode (location: (129...137))
         │   ├── opening_loc: (129...137) = "<<-\"EOF\""
         │   ├── parts: (length: 3)
@@ -135,28 +155,39 @@
         │   ├── content_loc: (175...178) = "abc"
         │   ├── closing_loc: (178...179) = "#"
         │   └── unescaped: "abc"
-        ├── @ StringNode (location: (181...187))
+        ├── @ InterpolatedStringNode (location: (181...187))
         │   ├── opening_loc: (181...187) = "<<-EOF"
-        │   ├── content_loc: (188...196) = "  a\n  b\n"
-        │   ├── closing_loc: (196...200) = "EOF\n"
-        │   └── unescaped: "  a\n  b\n"
-        ├── @ StringNode (location: (201...206))
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (188...196))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (188...196) = "  a\n  b\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "  a\n  b\n"
+        │   └── closing_loc: (196...200) = "EOF\n"
+        ├── @ InterpolatedStringNode (location: (201...206))
         │   ├── opening_loc: (201...206) = "<<-''"
-        │   ├── content_loc: (206...207) = "\n"
-        │   ├── closing_loc: (207...208) = "\n"
-        │   └── unescaped: ""
-        ├── @ StringNode (location: (209...217))
+        │   ├── parts: (length: 0)
+        │   └── closing_loc: (207...208) = "\n"
+        ├── @ InterpolatedStringNode (location: (209...217))
         │   ├── opening_loc: (209...217) = "<<-'EOF'"
-        │   ├── content_loc: (218...227) = "  a \#{1}\n"
-        │   ├── closing_loc: (227...231) = "EOF\n"
-        │   └── unescaped: "  a \#{1}\n"
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (218...227))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (218...227) = "  a \#{1}\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "  a \#{1}\n"
+        │   └── closing_loc: (227...231) = "EOF\n"
         ├── @ CallNode (location: (232...243))
         │   ├── receiver:
-        │   │   @ StringNode (location: (232...236))
+        │   │   @ InterpolatedStringNode (location: (232...236))
         │   │   ├── opening_loc: (232...236) = "<<-A"
-        │   │   ├── content_loc: (244...248) = "  a\n"
-        │   │   ├── closing_loc: (248...250) = "A\n"
-        │   │   └── unescaped: "  a\n"
+        │   │   ├── parts: (length: 1)
+        │   │   │   └── @ StringNode (location: (244...248))
+        │   │   │       ├── opening_loc: ∅
+        │   │   │       ├── content_loc: (244...248) = "  a\n"
+        │   │   │       ├── closing_loc: ∅
+        │   │   │       └── unescaped: "  a\n"
+        │   │   └── closing_loc: (248...250) = "A\n"
         │   ├── call_operator_loc: ∅
         │   ├── message_loc: (237...238) = "+"
         │   ├── opening_loc: ∅
@@ -191,11 +222,15 @@
         │   └── name: "+"
         └── @ CallNode (location: (267...278))
             ├── receiver:
-            │   @ StringNode (location: (267...271))
+            │   @ InterpolatedStringNode (location: (267...271))
             │   ├── opening_loc: (267...271) = "<<-A"
-            │   ├── content_loc: (279...283) = "  a\n"
-            │   ├── closing_loc: (283...285) = "A\n"
-            │   └── unescaped: "  a\n"
+            │   ├── parts: (length: 1)
+            │   │   └── @ StringNode (location: (279...283))
+            │   │       ├── opening_loc: ∅
+            │   │       ├── content_loc: (279...283) = "  a\n"
+            │   │       ├── closing_loc: ∅
+            │   │       └── unescaped: "  a\n"
+            │   └── closing_loc: (283...285) = "A\n"
             ├── call_operator_loc: ∅
             ├── message_loc: (272...273) = "+"
             ├── opening_loc: ∅
diff --git a/test/yarp/snapshots/dos_endings.txt b/test/yarp/snapshots/dos_endings.txt
index 7afc5d3e..5186b4eb 100644
--- a/test/yarp/snapshots/dos_endings.txt
+++ b/test/yarp/snapshots/dos_endings.txt
@@ -37,11 +37,15 @@
         │   │       └── unescaped: "ab"
         │   ├── opening_loc: (28...31) = "%I{"
         │   └── closing_loc: (36...37) = "}"
-        ├── @ StringNode (location: (41...45))
+        ├── @ InterpolatedStringNode (location: (41...45))
         │   ├── opening_loc: (41...45) = "<<-E"
-        │   ├── content_loc: (47...70) = "    1 \\\r\n    2\r\n    3\r\n"
-        │   ├── closing_loc: (70...73) = "E\r\n"
-        │   └── unescaped: "    1     2\r\n    3\r\n"
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (47...70))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (47...70) = "    1 \\\r\n    2\r\n    3\r\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "    1     2\r\n    3\r\n"
+        │   └── closing_loc: (70...73) = "E\r\n"
         ├── @ LocalVariableWriteNode (location: (75...84))
         │   ├── name: :x
         │   ├── depth: 0
@@ -68,11 +72,15 @@
             │   │   └── arguments: (length: 1)
             │   │       └── @ CallNode (location: (96...107))
             │   │           ├── receiver:
-            │   │           │   @ StringNode (location: (96...102))
+            │   │           │   @ InterpolatedStringNode (location: (96...102))
             │   │           │   ├── opening_loc: (96...102) = "<<~EOF"
-            │   │           │   ├── content_loc: (110...121) = "\r\n    baz\r\n"
-            │   │           │   ├── closing_loc: (121...128) = "  EOF\r\n"
-            │   │           │   └── unescaped: "\nbaz\r\n"
+            │   │           │   ├── parts: (length: 1)
+            │   │           │   │   └── @ StringNode (location: (110...121))
+            │   │           │   │       ├── opening_loc: ∅
+            │   │           │   │       ├── content_loc: (110...121) = "\r\n    baz\r\n"
+            │   │           │   │       ├── closing_loc: ∅
+            │   │           │   │       └── unescaped: "\nbaz\r\n"
+            │   │           │   └── closing_loc: (121...128) = "  EOF\r\n"
             │   │           ├── call_operator_loc: (102...103) = "."
             │   │           ├── message_loc: (103...107) = "chop"
             │   │           ├── opening_loc: ∅
diff --git a/test/yarp/snapshots/heredoc_with_escaped_newline_at_start.txt b/test/yarp/snapshots/heredoc_with_escaped_newline_at_start.txt
index 5f7cbeda..ed2abc7f 100644
--- a/test/yarp/snapshots/heredoc_with_escaped_newline_at_start.txt
+++ b/test/yarp/snapshots/heredoc_with_escaped_newline_at_start.txt
@@ -5,11 +5,10 @@
     └── body: (length: 2)
         ├── @ CallNode (location: (0...25))
         │   ├── receiver:
-        │   │   @ StringNode (location: (0...9))
+        │   │   @ InterpolatedStringNode (location: (0...9))
         │   │   ├── opening_loc: (0...9) = "<<-TARGET"
-        │   │   ├── content_loc: (9...27) = ".gsub /^\\s{/, ''\\\n"
-        │   │   ├── closing_loc: (27...34) = "TARGET\n"
-        │   │   └── unescaped: ""
+        │   │   ├── parts: (length: 0)
+        │   │   └── closing_loc: (27...34) = "TARGET\n"
         │   ├── call_operator_loc: (9...10) = "."
         │   ├── message_loc: (10...14) = "gsub"
         │   ├── opening_loc: ∅
@@ -33,11 +32,10 @@
         │   └── name: "gsub"
         └── @ CallNode (location: (37...62))
             ├── receiver:
-            │   @ StringNode (location: (37...46))
+            │   @ InterpolatedStringNode (location: (37...46))
             │   ├── opening_loc: (37...46) = "<<-TARGET"
-            │   ├── content_loc: (46...65) = ".gsub /^\\s{/, ''\\\r\n"
-            │   ├── closing_loc: (65...73) = "TARGET\r\n"
-            │   └── unescaped: ""
+            │   ├── parts: (length: 0)
+            │   └── closing_loc: (65...73) = "TARGET\r\n"
             ├── call_operator_loc: (46...47) = "."
             ├── message_loc: (47...51) = "gsub"
             ├── opening_loc: ∅
diff --git a/test/yarp/snapshots/heredoc_with_trailing_newline.txt b/test/yarp/snapshots/heredoc_with_trailing_newline.txt
index 077e9703..cf80947b 100644
--- a/test/yarp/snapshots/heredoc_with_trailing_newline.txt
+++ b/test/yarp/snapshots/heredoc_with_trailing_newline.txt
@@ -3,8 +3,7 @@
 └── statements:
     @ StatementsNode (location: (0...6))
     └── body: (length: 1)
-        └── @ StringNode (location: (0...6))
+        └── @ InterpolatedStringNode (location: (0...6))
             ├── opening_loc: (0...6) = "<<-END"
-            ├── content_loc: (6...7) = "\n"
-            ├── closing_loc: (7...10) = "END"
-            └── unescaped: ""
+            ├── parts: (length: 0)
+            └── closing_loc: (7...10) = "END"
diff --git a/test/yarp/snapshots/heredocs_nested.txt b/test/yarp/snapshots/heredocs_nested.txt
index 33511079..a35b0bf4 100644
--- a/test/yarp/snapshots/heredocs_nested.txt
+++ b/test/yarp/snapshots/heredocs_nested.txt
@@ -16,11 +16,15 @@
             │   │   ├── statements:
             │   │   │   @ StatementsNode (location: (15...21))
             │   │   │   └── body: (length: 1)
-            │   │   │       └── @ StringNode (location: (15...21))
+            │   │   │       └── @ InterpolatedStringNode (location: (15...21))
             │   │   │           ├── opening_loc: (15...21) = "<<RUBY"
-            │   │   │           ├── content_loc: (22...30) = "  hello\n"
-            │   │   │           ├── closing_loc: (30...35) = "RUBY\n"
-            │   │   │           └── unescaped: "  hello\n"
+            │   │   │           ├── parts: (length: 1)
+            │   │   │           │   └── @ StringNode (location: (22...30))
+            │   │   │           │       ├── opening_loc: ∅
+            │   │   │           │       ├── content_loc: (22...30) = "  hello\n"
+            │   │   │           │       ├── closing_loc: ∅
+            │   │   │           │       └── unescaped: "  hello\n"
+            │   │   │           └── closing_loc: (30...35) = "RUBY\n"
             │   │   └── closing_loc: (35...36) = "}"
             │   └── @ StringNode (location: (36...42))
             │       ├── opening_loc: ∅
diff --git a/test/yarp/snapshots/heredocs_with_ignored_newlines.txt b/test/yarp/snapshots/heredocs_with_ignored_newlines.txt
index c0e84a87..e8e66162 100644
--- a/test/yarp/snapshots/heredocs_with_ignored_newlines.txt
+++ b/test/yarp/snapshots/heredocs_with_ignored_newlines.txt
@@ -3,13 +3,16 @@
 └── statements:
     @ StatementsNode (location: (0...23))
     └── body: (length: 2)
-        ├── @ StringNode (location: (0...7))
+        ├── @ InterpolatedStringNode (location: (0...7))
         │   ├── opening_loc: (0...7) = "<<-HERE"
-        │   ├── content_loc: (7...9) = "\\\n"
-        │   ├── closing_loc: (9...14) = "HERE\n"
-        │   └── unescaped: ""
-        └── @ StringNode (location: (15...23))
+        │   ├── parts: (length: 0)
+        │   └── closing_loc: (9...14) = "HERE\n"
+        └── @ InterpolatedStringNode (location: (15...23))
             ├── opening_loc: (15...23) = "<<~THERE"
-            ├── content_loc: (25...100) = "  way over\n  <<HERE\n    not here\n  HERE\n\n  <<~BUT\\\n    but\n  BUT\n    there\n"
-            ├── closing_loc: (100...106) = "THERE\n"
-            └── unescaped: "way over\n<<HERE\n  not here\nHERE\n\n<<~BUT  but\nBUT\n  there\n"
+            ├── parts: (length: 1)
+            │   └── @ StringNode (location: (25...100))
+            │       ├── opening_loc: ∅
+            │       ├── content_loc: (25...100) = "  way over\n  <<HERE\n    not here\n  HERE\n\n  <<~BUT\\\n    but\n  BUT\n    there\n"
+            │       ├── closing_loc: ∅
+            │       └── unescaped: "way over\n<<HERE\n  not here\nHERE\n\n<<~BUT  but\nBUT\n  there\n"
+            └── closing_loc: (100...106) = "THERE\n"
diff --git a/test/yarp/snapshots/heredocs_with_ignored_newlines_and_non_empty.txt b/test/yarp/snapshots/heredocs_with_ignored_newlines_and_non_empty.txt
index dd8e620b..d5c0bf56 100644
--- a/test/yarp/snapshots/heredocs_with_ignored_newlines_and_non_empty.txt
+++ b/test/yarp/snapshots/heredocs_with_ignored_newlines_and_non_empty.txt
@@ -3,8 +3,12 @@
 └── statements:
     @ StatementsNode (location: (0...6))
     └── body: (length: 1)
-        └── @ StringNode (location: (0...6))
+        └── @ InterpolatedStringNode (location: (0...6))
             ├── opening_loc: (0...6) = "<<-EOE"
-            ├── content_loc: (7...23) = " some\n heredocs\n"
-            ├── closing_loc: (23...26) = "EOE"
-            └── unescaped: " some\n heredocs\n"
+            ├── parts: (length: 1)
+            │   └── @ StringNode (location: (7...23))
+            │       ├── opening_loc: ∅
+            │       ├── content_loc: (7...23) = " some\n heredocs\n"
+            │       ├── closing_loc: ∅
+            │       └── unescaped: " some\n heredocs\n"
+            └── closing_loc: (23...26) = "EOE"
diff --git a/test/yarp/snapshots/seattlerb/difficult0_.txt b/test/yarp/snapshots/seattlerb/difficult0_.txt
index c6a521d2..b763d090 100644
--- a/test/yarp/snapshots/seattlerb/difficult0_.txt
+++ b/test/yarp/snapshots/seattlerb/difficult0_.txt
@@ -15,11 +15,15 @@
             │           ├── receiver:
             │           │   @ CallNode (location: (2...26))
             │           │   ├── receiver:
-            │           │   │   @ StringNode (location: (2...8))
+            │           │   │   @ InterpolatedStringNode (location: (2...8))
             │           │   │   ├── opening_loc: (2...8) = "<<-END"
-            │           │   │   ├── content_loc: (12...16) = "  a\n"
-            │           │   │   ├── closing_loc: (16...22) = "  END\n"
-            │           │   │   └── unescaped: "  a\n"
+            │           │   │   ├── parts: (length: 1)
+            │           │   │   │   └── @ StringNode (location: (12...16))
+            │           │   │   │       ├── opening_loc: ∅
+            │           │   │   │       ├── content_loc: (12...16) = "  a\n"
+            │           │   │   │       ├── closing_loc: ∅
+            │           │   │   │       └── unescaped: "  a\n"
+            │           │   │   └── closing_loc: (16...22) = "  END\n"
             │           │   ├── call_operator_loc: ∅
             │           │   ├── message_loc: (8...9) = "+"
             │           │   ├── opening_loc: ∅
diff --git a/test/yarp/snapshots/seattlerb/heredoc__backslash_dos_format.txt b/test/yarp/snapshots/seattlerb/heredoc__backslash_dos_format.txt
index 7f58f36d..656cfdcd 100644
--- a/test/yarp/snapshots/seattlerb/heredoc__backslash_dos_format.txt
+++ b/test/yarp/snapshots/seattlerb/heredoc__backslash_dos_format.txt
@@ -8,9 +8,13 @@
             ├── depth: 0
             ├── name_loc: (0...3) = "str"
             ├── value:
-            │   @ StringNode (location: (6...12))
+            │   @ InterpolatedStringNode (location: (6...12))
             │   ├── opening_loc: (6...12) = "<<-XXX"
-            │   ├── content_loc: (14...30) = "before\\\r\nafter\r\n"
-            │   ├── closing_loc: (30...35) = "XXX\r\n"
-            │   └── unescaped: "beforeafter\r\n"
+            │   ├── parts: (length: 1)
+            │   │   └── @ StringNode (location: (14...30))
+            │   │       ├── opening_loc: ∅
+            │   │       ├── content_loc: (14...30) = "before\\\r\nafter\r\n"
+            │   │       ├── closing_loc: ∅
+            │   │       └── unescaped: "beforeafter\r\n"
+            │   └── closing_loc: (30...35) = "XXX\r\n"
             └── operator_loc: (4...5) = "="
diff --git a/test/yarp/snapshots/seattlerb/heredoc_backslash_nl.txt b/test/yarp/snapshots/seattlerb/heredoc_backslash_nl.txt
index 3980b4f1..4bb11c48 100644
--- a/test/yarp/snapshots/seattlerb/heredoc_backslash_nl.txt
+++ b/test/yarp/snapshots/seattlerb/heredoc_backslash_nl.txt
@@ -8,8 +8,12 @@
         │   ├── content_loc: (1...39) = "  why would someone do this? \\\n  blah\n"
         │   ├── closing_loc: (39...40) = "\""
         │   └── unescaped: "  why would someone do this?   blah\n"
-        └── @ StringNode (location: (42...49))
+        └── @ InterpolatedStringNode (location: (42...49))
             ├── opening_loc: (42...49) = "<<-DESC"
-            ├── content_loc: (50...88) = "  why would someone do this? \\\n  blah\n"
-            ├── closing_loc: (88...93) = "DESC\n"
-            └── unescaped: "  why would someone do this?   blah\n"
+            ├── parts: (length: 1)
+            │   └── @ StringNode (location: (50...88))
+            │       ├── opening_loc: ∅
+            │       ├── content_loc: (50...88) = "  why would someone do this? \\\n  blah\n"
+            │       ├── closing_loc: ∅
+            │       └── unescaped: "  why would someone do this?   blah\n"
+            └── closing_loc: (88...93) = "DESC\n"
diff --git a/test/yarp/snapshots/seattlerb/heredoc_bad_hex_escape.txt b/test/yarp/snapshots/seattlerb/heredoc_bad_hex_escape.txt
index 7b0933bb..c0af63a6 100644
--- a/test/yarp/snapshots/seattlerb/heredoc_bad_hex_escape.txt
+++ b/test/yarp/snapshots/seattlerb/heredoc_bad_hex_escape.txt
@@ -8,9 +8,13 @@
             ├── depth: 0
             ├── name_loc: (0...1) = "s"
             ├── value:
-            │   @ StringNode (location: (4...9))
+            │   @ InterpolatedStringNode (location: (4...9))
             │   ├── opening_loc: (4...9) = "<<eos"
-            │   ├── content_loc: (10...17) = "a\\xE9b\n"
-            │   ├── closing_loc: (17...21) = "eos\n"
-            │   └── unescaped: "a\xE9b\n"
+            │   ├── parts: (length: 1)
+            │   │   └── @ StringNode (location: (10...17))
+            │   │       ├── opening_loc: ∅
+            │   │       ├── content_loc: (10...17) = "a\\xE9b\n"
+            │   │       ├── closing_loc: ∅
+            │   │       └── unescaped: "a\xE9b\n"
+            │   └── closing_loc: (17...21) = "eos\n"
             └── operator_loc: (2...3) = "="
diff --git a/test/yarp/snapshots/seattlerb/heredoc_bad_oct_escape.txt b/test/yarp/snapshots/seattlerb/heredoc_bad_oct_escape.txt
index 5973e23d..aba1212f 100644
--- a/test/yarp/snapshots/seattlerb/heredoc_bad_oct_escape.txt
+++ b/test/yarp/snapshots/seattlerb/heredoc_bad_oct_escape.txt
@@ -8,9 +8,13 @@
             ├── depth: 0
             ├── name_loc: (0...1) = "s"
             ├── value:
-            │   @ StringNode (location: (4...10))
+            │   @ InterpolatedStringNode (location: (4...10))
             │   ├── opening_loc: (4...10) = "<<-EOS"
-            │   ├── content_loc: (11...23) = "a\\247b\ncöd\n"
-            │   ├── closing_loc: (23...27) = "EOS\n"
-            │   └── unescaped: "a\xA7b\ncöd\n"
+            │   ├── parts: (length: 1)
+            │   │   └── @ StringNode (location: (11...23))
+            │   │       ├── opening_loc: ∅
+            │   │       ├── content_loc: (11...23) = "a\\247b\ncöd\n"
+            │   │       ├── closing_loc: ∅
+            │   │       └── unescaped: "a\xA7b\ncöd\n"
+            │   └── closing_loc: (23...27) = "EOS\n"
             └── operator_loc: (2...3) = "="
diff --git a/test/yarp/snapshots/seattlerb/heredoc_comma_arg.txt b/test/yarp/snapshots/seattlerb/heredoc_comma_arg.txt
index 5cb26773..c4e52e17 100644
--- a/test/yarp/snapshots/seattlerb/heredoc_comma_arg.txt
+++ b/test/yarp/snapshots/seattlerb/heredoc_comma_arg.txt
@@ -14,10 +14,14 @@
         │   └── closing_loc: (16...17) = "]"
         └── @ ArrayNode (location: (19...47))
             ├── elements: (length: 1)
-            │   └── @ StringNode (location: (20...27))
+            │   └── @ InterpolatedStringNode (location: (20...27))
             │       ├── opening_loc: (20...27) = "<<-FILE"
-            │       ├── content_loc: (29...41) = "  some text\n"
-            │       ├── closing_loc: (41...46) = "FILE\n"
-            │       └── unescaped: "  some text\n"
+            │       ├── parts: (length: 1)
+            │       │   └── @ StringNode (location: (29...41))
+            │       │       ├── opening_loc: ∅
+            │       │       ├── content_loc: (29...41) = "  some text\n"
+            │       │       ├── closing_loc: ∅
+            │       │       └── unescaped: "  some text\n"
+            │       └── closing_loc: (41...46) = "FILE\n"
             ├── opening_loc: (19...20) = "["
             └── closing_loc: (46...47) = "]"
diff --git a/test/yarp/snapshots/seattlerb/heredoc_lineno.txt b/test/yarp/snapshots/seattlerb/heredoc_lineno.txt
index f7980d3e..8d050ae8 100644
--- a/test/yarp/snapshots/seattlerb/heredoc_lineno.txt
+++ b/test/yarp/snapshots/seattlerb/heredoc_lineno.txt
@@ -8,11 +8,15 @@
         │   ├── depth: 0
         │   ├── name_loc: (0...1) = "c"
         │   ├── value:
-        │   │   @ StringNode (location: (4...11))
+        │   │   @ InterpolatedStringNode (location: (4...11))
         │   │   ├── opening_loc: (4...11) = "<<'CCC'"
-        │   │   ├── content_loc: (12...30) = "line2\nline3\nline4\n"
-        │   │   ├── closing_loc: (30...34) = "CCC\n"
-        │   │   └── unescaped: "line2\nline3\nline4\n"
+        │   │   ├── parts: (length: 1)
+        │   │   │   └── @ StringNode (location: (12...30))
+        │   │   │       ├── opening_loc: ∅
+        │   │   │       ├── content_loc: (12...30) = "line2\nline3\nline4\n"
+        │   │   │       ├── closing_loc: ∅
+        │   │   │       └── unescaped: "line2\nline3\nline4\n"
+        │   │   └── closing_loc: (30...34) = "CCC\n"
         │   └── operator_loc: (2...3) = "="
         └── @ LocalVariableWriteNode (location: (35...41))
             ├── name: :d
diff --git a/test/yarp/snapshots/seattlerb/heredoc_nested.txt b/test/yarp/snapshots/seattlerb/heredoc_nested.txt
index ad5c1a88..e3d44d38 100644
--- a/test/yarp/snapshots/seattlerb/heredoc_nested.txt
+++ b/test/yarp/snapshots/seattlerb/heredoc_nested.txt
@@ -13,11 +13,15 @@
             │   │   │   │   ├── statements:
             │   │   │   │   │   @ StatementsNode (location: (8...11))
             │   │   │   │   │   └── body: (length: 1)
-            │   │   │   │   │       └── @ StringNode (location: (8...11))
+            │   │   │   │   │       └── @ InterpolatedStringNode (location: (8...11))
             │   │   │   │   │           ├── opening_loc: (8...11) = "<<B"
-            │   │   │   │   │           ├── content_loc: (13...15) = "b\n"
-            │   │   │   │   │           ├── closing_loc: (15...17) = "B\n"
-            │   │   │   │   │           └── unescaped: "b\n"
+            │   │   │   │   │           ├── parts: (length: 1)
+            │   │   │   │   │           │   └── @ StringNode (location: (13...15))
+            │   │   │   │   │           │       ├── opening_loc: ∅
+            │   │   │   │   │           │       ├── content_loc: (13...15) = "b\n"
+            │   │   │   │   │           │       ├── closing_loc: ∅
+            │   │   │   │   │           │       └── unescaped: "b\n"
+            │   │   │   │   │           └── closing_loc: (15...17) = "B\n"
             │   │   │   │   └── closing_loc: (11...12) = "}"
             │   │   │   ├── @ StringNode (location: (12...13))
             │   │   │   │   ├── opening_loc: ∅
diff --git a/test/yarp/snapshots/seattlerb/heredoc_squiggly.txt b/test/yarp/snapshots/seattlerb/heredoc_squiggly.txt
index 0edb96fa..789e3d90 100644
--- a/test/yarp/snapshots/seattlerb/heredoc_squiggly.txt
+++ b/test/yarp/snapshots/seattlerb/heredoc_squiggly.txt
@@ -8,9 +8,13 @@
             ├── depth: 0
             ├── name_loc: (0...1) = "a"
             ├── value:
-            │   @ StringNode (location: (4...12))
+            │   @ InterpolatedStringNode (location: (4...12))
             │   ├── opening_loc: (4...12) = "<<~\"EOF\""
-            │   ├── content_loc: (13...25) = "  x\n  y\n  z\n"
-            │   ├── closing_loc: (25...31) = "  EOF\n"
-            │   └── unescaped: "x\ny\nz\n"
+            │   ├── parts: (length: 1)
+            │   │   └── @ StringNode (location: (13...25))
+            │   │       ├── opening_loc: ∅
+            │   │       ├── content_loc: (13...25) = "  x\n  y\n  z\n"
+            │   │       ├── closing_loc: ∅
+            │   │       └── unescaped: "x\ny\nz\n"
+            │   └── closing_loc: (25...31) = "  EOF\n"
             └── operator_loc: (2...3) = "="
diff --git a/test/yarp/snapshots/seattlerb/heredoc_squiggly_blank_lines.txt b/test/yarp/snapshots/seattlerb/heredoc_squiggly_blank_lines.txt
index 57864a67..fd474553 100644
--- a/test/yarp/snapshots/seattlerb/heredoc_squiggly_blank_lines.txt
+++ b/test/yarp/snapshots/seattlerb/heredoc_squiggly_blank_lines.txt
@@ -8,9 +8,13 @@
             ├── depth: 0
             ├── name_loc: (0...1) = "a"
             ├── value:
-            │   @ StringNode (location: (4...10))
+            │   @ InterpolatedStringNode (location: (4...10))
             │   ├── opening_loc: (4...10) = "<<~EOF"
-            │   ├── content_loc: (11...20) = "  x\n\n  z\n"
-            │   ├── closing_loc: (20...24) = "EOF\n"
-            │   └── unescaped: "x\n\nz\n"
+            │   ├── parts: (length: 1)
+            │   │   └── @ StringNode (location: (11...20))
+            │   │       ├── opening_loc: ∅
+            │   │       ├── content_loc: (11...20) = "  x\n\n  z\n"
+            │   │       ├── closing_loc: ∅
+            │   │       └── unescaped: "x\n\nz\n"
+            │   └── closing_loc: (20...24) = "EOF\n"
             └── operator_loc: (2...3) = "="
diff --git a/test/yarp/snapshots/seattlerb/heredoc_squiggly_empty.txt b/test/yarp/snapshots/seattlerb/heredoc_squiggly_empty.txt
index 78e9731f..7769e4ca 100644
--- a/test/yarp/snapshots/seattlerb/heredoc_squiggly_empty.txt
+++ b/test/yarp/snapshots/seattlerb/heredoc_squiggly_empty.txt
@@ -3,8 +3,7 @@
 └── statements:
     @ StatementsNode (location: (0...4))
     └── body: (length: 1)
-        └── @ StringNode (location: (0...4))
+        └── @ InterpolatedStringNode (location: (0...4))
             ├── opening_loc: (0...4) = "<<~A"
-            ├── content_loc: (4...5) = "\n"
-            ├── closing_loc: (5...7) = "A\n"
-            └── unescaped: ""
+            ├── parts: (length: 0)
+            └── closing_loc: (5...7) = "A\n"
diff --git a/test/yarp/snapshots/seattlerb/heredoc_squiggly_no_indent.txt b/test/yarp/snapshots/seattlerb/heredoc_squiggly_no_indent.txt
index e4b9c6cc..b88a5b4a 100644
--- a/test/yarp/snapshots/seattlerb/heredoc_squiggly_no_indent.txt
+++ b/test/yarp/snapshots/seattlerb/heredoc_squiggly_no_indent.txt
@@ -3,8 +3,12 @@
 └── statements:
     @ StatementsNode (location: (0...4))
     └── body: (length: 1)
-        └── @ StringNode (location: (0...4))
+        └── @ InterpolatedStringNode (location: (0...4))
             ├── opening_loc: (0...4) = "<<~A"
-            ├── content_loc: (5...7) = "a\n"
-            ├── closing_loc: (7...9) = "A\n"
-            └── unescaped: "a\n"
+            ├── parts: (length: 1)
+            │   └── @ StringNode (location: (5...7))
+            │       ├── opening_loc: ∅
+            │       ├── content_loc: (5...7) = "a\n"
+            │       ├── closing_loc: ∅
+            │       └── unescaped: "a\n"
+            └── closing_loc: (7...9) = "A\n"
diff --git a/test/yarp/snapshots/seattlerb/heredoc_squiggly_tabs.txt b/test/yarp/snapshots/seattlerb/heredoc_squiggly_tabs.txt
index 47feda9a..b5046bcc 100644
--- a/test/yarp/snapshots/seattlerb/heredoc_squiggly_tabs.txt
+++ b/test/yarp/snapshots/seattlerb/heredoc_squiggly_tabs.txt
@@ -8,9 +8,13 @@
             ├── depth: 0
             ├── name_loc: (0...1) = "a"
             ├── value:
-            │   @ StringNode (location: (4...12))
+            │   @ InterpolatedStringNode (location: (4...12))
             │   ├── opening_loc: (4...12) = "<<~\"EOF\""
-            │   ├── content_loc: (13...43) = "        blah blah\n\t blah blah\n"
-            │   ├── closing_loc: (43...49) = "  EOF\n"
-            │   └── unescaped: "blah blah\n blah blah\n"
+            │   ├── parts: (length: 1)
+            │   │   └── @ StringNode (location: (13...43))
+            │   │       ├── opening_loc: ∅
+            │   │       ├── content_loc: (13...43) = "        blah blah\n\t blah blah\n"
+            │   │       ├── closing_loc: ∅
+            │   │       └── unescaped: "blah blah\n blah blah\n"
+            │   └── closing_loc: (43...49) = "  EOF\n"
             └── operator_loc: (2...3) = "="
diff --git a/test/yarp/snapshots/seattlerb/heredoc_squiggly_tabs_extra.txt b/test/yarp/snapshots/seattlerb/heredoc_squiggly_tabs_extra.txt
index 7d9b7da5..f321ec6e 100644
--- a/test/yarp/snapshots/seattlerb/heredoc_squiggly_tabs_extra.txt
+++ b/test/yarp/snapshots/seattlerb/heredoc_squiggly_tabs_extra.txt
@@ -8,9 +8,13 @@
             ├── depth: 0
             ├── name_loc: (0...1) = "a"
             ├── value:
-            │   @ StringNode (location: (4...12))
+            │   @ InterpolatedStringNode (location: (4...12))
             │   ├── opening_loc: (4...12) = "<<~\"EOF\""
-            │   ├── content_loc: (13...37) = "  blah blah\n \tblah blah\n"
-            │   ├── closing_loc: (37...43) = "  EOF\n"
-            │   └── unescaped: "blah blah\n\tblah blah\n"
+            │   ├── parts: (length: 1)
+            │   │   └── @ StringNode (location: (13...37))
+            │   │       ├── opening_loc: ∅
+            │   │       ├── content_loc: (13...37) = "  blah blah\n \tblah blah\n"
+            │   │       ├── closing_loc: ∅
+            │   │       └── unescaped: "blah blah\n\tblah blah\n"
+            │   └── closing_loc: (37...43) = "  EOF\n"
             └── operator_loc: (2...3) = "="
diff --git a/test/yarp/snapshots/seattlerb/heredoc_squiggly_visually_blank_lines.txt b/test/yarp/snapshots/seattlerb/heredoc_squiggly_visually_blank_lines.txt
index aa337be5..8fbdfac6 100644
--- a/test/yarp/snapshots/seattlerb/heredoc_squiggly_visually_blank_lines.txt
+++ b/test/yarp/snapshots/seattlerb/heredoc_squiggly_visually_blank_lines.txt
@@ -8,9 +8,13 @@
             ├── depth: 0
             ├── name_loc: (0...1) = "a"
             ├── value:
-            │   @ StringNode (location: (4...10))
+            │   @ InterpolatedStringNode (location: (4...10))
             │   ├── opening_loc: (4...10) = "<<~EOF"
-            │   ├── content_loc: (11...21) = "  x\n \n  z\n"
-            │   ├── closing_loc: (21...25) = "EOF\n"
-            │   └── unescaped: "x\n\nz\n"
+            │   ├── parts: (length: 1)
+            │   │   └── @ StringNode (location: (11...21))
+            │   │       ├── opening_loc: ∅
+            │   │       ├── content_loc: (11...21) = "  x\n \n  z\n"
+            │   │       ├── closing_loc: ∅
+            │   │       └── unescaped: "x\n\nz\n"
+            │   └── closing_loc: (21...25) = "EOF\n"
             └── operator_loc: (2...3) = "="
diff --git a/test/yarp/snapshots/seattlerb/heredoc_trailing_slash_continued_call.txt b/test/yarp/snapshots/seattlerb/heredoc_trailing_slash_continued_call.txt
index ed21660c..f5018d99 100644
--- a/test/yarp/snapshots/seattlerb/heredoc_trailing_slash_continued_call.txt
+++ b/test/yarp/snapshots/seattlerb/heredoc_trailing_slash_continued_call.txt
@@ -5,11 +5,15 @@
     └── body: (length: 1)
         └── @ CallNode (location: (0...22))
             ├── receiver:
-            │   @ StringNode (location: (0...5))
+            │   @ InterpolatedStringNode (location: (0...5))
             │   ├── opening_loc: (0...5) = "<<END"
-            │   ├── content_loc: (7...12) = "blah\n"
-            │   ├── closing_loc: (12...16) = "END\n"
-            │   └── unescaped: "blah\n"
+            │   ├── parts: (length: 1)
+            │   │   └── @ StringNode (location: (7...12))
+            │   │       ├── opening_loc: ∅
+            │   │       ├── content_loc: (7...12) = "blah\n"
+            │   │       ├── closing_loc: ∅
+            │   │       └── unescaped: "blah\n"
+            │   └── closing_loc: (12...16) = "END\n"
             ├── call_operator_loc: (16...17) = "."
             ├── message_loc: (17...22) = "strip"
             ├── opening_loc: ∅
diff --git a/test/yarp/snapshots/seattlerb/heredoc_unicode.txt b/test/yarp/snapshots/seattlerb/heredoc_unicode.txt
index 95a04c3e..0fc87372 100644
--- a/test/yarp/snapshots/seattlerb/heredoc_unicode.txt
+++ b/test/yarp/snapshots/seattlerb/heredoc_unicode.txt
@@ -3,8 +3,12 @@
 └── statements:
     @ StatementsNode (location: (0...9))
     └── body: (length: 1)
-        └── @ StringNode (location: (0...9))
+        └── @ InterpolatedStringNode (location: (0...9))
             ├── opening_loc: (0...9) = "<<OOTPÜT"
-            ├── content_loc: (10...12) = ".\n"
-            ├── closing_loc: (12...20) = "OOTPÜT\n"
-            └── unescaped: ".\n"
+            ├── parts: (length: 1)
+            │   └── @ StringNode (location: (10...12))
+            │       ├── opening_loc: ∅
+            │       ├── content_loc: (10...12) = ".\n"
+            │       ├── closing_loc: ∅
+            │       └── unescaped: ".\n"
+            └── closing_loc: (12...20) = "OOTPÜT\n"
diff --git a/test/yarp/snapshots/seattlerb/heredoc_with_carriage_return_escapes.txt b/test/yarp/snapshots/seattlerb/heredoc_with_carriage_return_escapes.txt
index a19f37c6..b5f85f43 100644
--- a/test/yarp/snapshots/seattlerb/heredoc_with_carriage_return_escapes.txt
+++ b/test/yarp/snapshots/seattlerb/heredoc_with_carriage_return_escapes.txt
@@ -3,8 +3,12 @@
 └── statements:
     @ StatementsNode (location: (0...5))
     └── body: (length: 1)
-        └── @ StringNode (location: (0...5))
+        └── @ InterpolatedStringNode (location: (0...5))
             ├── opening_loc: (0...5) = "<<EOS"
-            ├── content_loc: (6...21) = "foo\\rbar\nbaz\\r\n"
-            ├── closing_loc: (21...25) = "EOS\n"
-            └── unescaped: "foo\rbar\nbaz\r\n"
+            ├── parts: (length: 1)
+            │   └── @ StringNode (location: (6...21))
+            │       ├── opening_loc: ∅
+            │       ├── content_loc: (6...21) = "foo\\rbar\nbaz\\r\n"
+            │       ├── closing_loc: ∅
+            │       └── unescaped: "foo\rbar\nbaz\r\n"
+            └── closing_loc: (21...25) = "EOS\n"
diff --git a/test/yarp/snapshots/seattlerb/heredoc_with_carriage_return_escapes_windows.txt b/test/yarp/snapshots/seattlerb/heredoc_with_carriage_return_escapes_windows.txt
index 558fac8c..aed5f046 100644
--- a/test/yarp/snapshots/seattlerb/heredoc_with_carriage_return_escapes_windows.txt
+++ b/test/yarp/snapshots/seattlerb/heredoc_with_carriage_return_escapes_windows.txt
@@ -3,8 +3,12 @@
 └── statements:
     @ StatementsNode (location: (0...5))
     └── body: (length: 1)
-        └── @ StringNode (location: (0...5))
+        └── @ InterpolatedStringNode (location: (0...5))
             ├── opening_loc: (0...5) = "<<EOS"
-            ├── content_loc: (7...24) = "foo\\rbar\r\nbaz\\r\r\n"
-            ├── closing_loc: (24...29) = "EOS\r\n"
-            └── unescaped: "foo\rbar\r\nbaz\r\r\n"
+            ├── parts: (length: 1)
+            │   └── @ StringNode (location: (7...24))
+            │       ├── opening_loc: ∅
+            │       ├── content_loc: (7...24) = "foo\\rbar\r\nbaz\\r\r\n"
+            │       ├── closing_loc: ∅
+            │       └── unescaped: "foo\rbar\r\nbaz\r\r\n"
+            └── closing_loc: (24...29) = "EOS\r\n"
diff --git a/test/yarp/snapshots/seattlerb/heredoc_with_extra_carriage_horrible_mix.txt b/test/yarp/snapshots/seattlerb/heredoc_with_extra_carriage_horrible_mix.txt
index 97270b40..9c6471da 100644
--- a/test/yarp/snapshots/seattlerb/heredoc_with_extra_carriage_horrible_mix.txt
+++ b/test/yarp/snapshots/seattlerb/heredoc_with_extra_carriage_horrible_mix.txt
@@ -3,8 +3,12 @@
 └── statements:
     @ StatementsNode (location: (0...7))
     └── body: (length: 1)
-        └── @ StringNode (location: (0...7))
+        └── @ InterpolatedStringNode (location: (0...7))
             ├── opening_loc: (0...7) = "<<'eot'"
-            ├── content_loc: (9...15) = "body\r\n"
-            ├── closing_loc: (15...19) = "eot\n"
-            └── unescaped: "body\r\n"
+            ├── parts: (length: 1)
+            │   └── @ StringNode (location: (9...15))
+            │       ├── opening_loc: ∅
+            │       ├── content_loc: (9...15) = "body\r\n"
+            │       ├── closing_loc: ∅
+            │       └── unescaped: "body\r\n"
+            └── closing_loc: (15...19) = "eot\n"
diff --git a/test/yarp/snapshots/seattlerb/heredoc_with_extra_carriage_returns.txt b/test/yarp/snapshots/seattlerb/heredoc_with_extra_carriage_returns.txt
index 56e12ee8..217824be 100644
--- a/test/yarp/snapshots/seattlerb/heredoc_with_extra_carriage_returns.txt
+++ b/test/yarp/snapshots/seattlerb/heredoc_with_extra_carriage_returns.txt
@@ -3,8 +3,12 @@
 └── statements:
     @ StatementsNode (location: (0...5))
     └── body: (length: 1)
-        └── @ StringNode (location: (0...5))
+        └── @ InterpolatedStringNode (location: (0...5))
             ├── opening_loc: (0...5) = "<<EOS"
-            ├── content_loc: (6...19) = "foo\rbar\r\nbaz\n"
-            ├── closing_loc: (19...23) = "EOS\n"
-            └── unescaped: "foo\rbar\r\nbaz\n"
+            ├── parts: (length: 1)
+            │   └── @ StringNode (location: (6...19))
+            │       ├── opening_loc: ∅
+            │       ├── content_loc: (6...19) = "foo\rbar\r\nbaz\n"
+            │       ├── closing_loc: ∅
+            │       └── unescaped: "foo\rbar\r\nbaz\n"
+            └── closing_loc: (19...23) = "EOS\n"
diff --git a/test/yarp/snapshots/seattlerb/heredoc_with_extra_carriage_returns_windows.txt b/test/yarp/snapshots/seattlerb/heredoc_with_extra_carriage_returns_windows.txt
index e8d05713..482dc5ba 100644
--- a/test/yarp/snapshots/seattlerb/heredoc_with_extra_carriage_returns_windows.txt
+++ b/test/yarp/snapshots/seattlerb/heredoc_with_extra_carriage_returns_windows.txt
@@ -3,8 +3,12 @@
 └── statements:
     @ StatementsNode (location: (0...5))
     └── body: (length: 1)
-        └── @ StringNode (location: (0...5))
+        └── @ InterpolatedStringNode (location: (0...5))
             ├── opening_loc: (0...5) = "<<EOS"
-            ├── content_loc: (7...22) = "foo\rbar\r\r\nbaz\r\n"
-            ├── closing_loc: (22...27) = "EOS\r\n"
-            └── unescaped: "foo\rbar\r\r\nbaz\r\n"
+            ├── parts: (length: 1)
+            │   └── @ StringNode (location: (7...22))
+            │       ├── opening_loc: ∅
+            │       ├── content_loc: (7...22) = "foo\rbar\r\r\nbaz\r\n"
+            │       ├── closing_loc: ∅
+            │       └── unescaped: "foo\rbar\r\r\nbaz\r\n"
+            └── closing_loc: (22...27) = "EOS\r\n"
diff --git a/test/yarp/snapshots/seattlerb/heredoc_with_not_global_interpolation.txt b/test/yarp/snapshots/seattlerb/heredoc_with_not_global_interpolation.txt
index 50a92cc0..4f51527b 100644
--- a/test/yarp/snapshots/seattlerb/heredoc_with_not_global_interpolation.txt
+++ b/test/yarp/snapshots/seattlerb/heredoc_with_not_global_interpolation.txt
@@ -3,8 +3,12 @@
 └── statements:
     @ StatementsNode (location: (0...10))
     └── body: (length: 1)
-        └── @ StringNode (location: (0...10))
+        └── @ InterpolatedStringNode (location: (0...10))
             ├── opening_loc: (0...10) = "<<-HEREDOC"
-            ├── content_loc: (11...15) = "\#${\n"
-            ├── closing_loc: (15...23) = "HEREDOC\n"
-            └── unescaped: "\#${\n"
+            ├── parts: (length: 1)
+            │   └── @ StringNode (location: (11...15))
+            │       ├── opening_loc: ∅
+            │       ├── content_loc: (11...15) = "\#${\n"
+            │       ├── closing_loc: ∅
+            │       └── unescaped: "\#${\n"
+            └── closing_loc: (15...23) = "HEREDOC\n"
diff --git a/test/yarp/snapshots/seattlerb/heredoc_with_only_carriage_returns.txt b/test/yarp/snapshots/seattlerb/heredoc_with_only_carriage_returns.txt
index 8a5a0e4c..ea9bc76b 100644
--- a/test/yarp/snapshots/seattlerb/heredoc_with_only_carriage_returns.txt
+++ b/test/yarp/snapshots/seattlerb/heredoc_with_only_carriage_returns.txt
@@ -3,8 +3,12 @@
 └── statements:
     @ StatementsNode (location: (0...5))
     └── body: (length: 1)
-        └── @ StringNode (location: (0...5))
+        └── @ InterpolatedStringNode (location: (0...5))
             ├── opening_loc: (0...5) = "<<EOS"
-            ├── content_loc: (6...14) = "\r\n\r\r\n\\r\n"
-            ├── closing_loc: (14...18) = "EOS\n"
-            └── unescaped: "\r\n\r\r\n\r\n"
+            ├── parts: (length: 1)
+            │   └── @ StringNode (location: (6...14))
+            │       ├── opening_loc: ∅
+            │       ├── content_loc: (6...14) = "\r\n\r\r\n\\r\n"
+            │       ├── closing_loc: ∅
+            │       └── unescaped: "\r\n\r\r\n\r\n"
+            └── closing_loc: (14...18) = "EOS\n"
diff --git a/test/yarp/snapshots/seattlerb/heredoc_with_only_carriage_returns_windows.txt b/test/yarp/snapshots/seattlerb/heredoc_with_only_carriage_returns_windows.txt
index 98929516..8a57f8cf 100644
--- a/test/yarp/snapshots/seattlerb/heredoc_with_only_carriage_returns_windows.txt
+++ b/test/yarp/snapshots/seattlerb/heredoc_with_only_carriage_returns_windows.txt
@@ -3,8 +3,12 @@
 └── statements:
     @ StatementsNode (location: (0...5))
     └── body: (length: 1)
-        └── @ StringNode (location: (0...5))
+        └── @ InterpolatedStringNode (location: (0...5))
             ├── opening_loc: (0...5) = "<<EOS"
-            ├── content_loc: (7...18) = "\r\r\n\r\r\r\n\\r\r\n"
-            ├── closing_loc: (18...23) = "EOS\r\n"
-            └── unescaped: "\r\r\n\r\r\r\n\r\r\n"
+            ├── parts: (length: 1)
+            │   └── @ StringNode (location: (7...18))
+            │       ├── opening_loc: ∅
+            │       ├── content_loc: (7...18) = "\r\r\n\r\r\r\n\\r\r\n"
+            │       ├── closing_loc: ∅
+            │       └── unescaped: "\r\r\n\r\r\r\n\r\r\n"
+            └── closing_loc: (18...23) = "EOS\r\n"
diff --git a/test/yarp/snapshots/seattlerb/parse_line_heredoc.txt b/test/yarp/snapshots/seattlerb/parse_line_heredoc.txt
index 4b2a57e3..4e0d5e2f 100644
--- a/test/yarp/snapshots/seattlerb/parse_line_heredoc.txt
+++ b/test/yarp/snapshots/seattlerb/parse_line_heredoc.txt
@@ -10,11 +10,15 @@
         │   ├── value:
         │   │   @ CallNode (location: (15...31))
         │   │   ├── receiver:
-        │   │   │   @ StringNode (location: (15...25))
+        │   │   │   @ InterpolatedStringNode (location: (15...25))
         │   │   │   ├── opening_loc: (15...25) = "<<-HEREDOC"
-        │   │   │   ├── content_loc: (32...57) = "        very long string\n"
-        │   │   │   ├── closing_loc: (57...71) = "      HEREDOC\n"
-        │   │   │   └── unescaped: "        very long string\n"
+        │   │   │   ├── parts: (length: 1)
+        │   │   │   │   └── @ StringNode (location: (32...57))
+        │   │   │   │       ├── opening_loc: ∅
+        │   │   │   │       ├── content_loc: (32...57) = "        very long string\n"
+        │   │   │   │       ├── closing_loc: ∅
+        │   │   │   │       └── unescaped: "        very long string\n"
+        │   │   │   └── closing_loc: (57...71) = "      HEREDOC\n"
         │   │   ├── call_operator_loc: (25...26) = "."
         │   │   ├── message_loc: (26...31) = "strip"
         │   │   ├── opening_loc: ∅
diff --git a/test/yarp/snapshots/seattlerb/parse_line_heredoc_hardnewline.txt b/test/yarp/snapshots/seattlerb/parse_line_heredoc_hardnewline.txt
index 29d8c9c6..c264873f 100644
--- a/test/yarp/snapshots/seattlerb/parse_line_heredoc_hardnewline.txt
+++ b/test/yarp/snapshots/seattlerb/parse_line_heredoc_hardnewline.txt
@@ -3,11 +3,15 @@
 └── statements:
     @ StatementsNode (location: (0...48))
     └── body: (length: 2)
-        ├── @ StringNode (location: (0...8))
+        ├── @ InterpolatedStringNode (location: (0...8))
         │   ├── opening_loc: (0...8) = "<<-EOFOO"
-        │   ├── content_loc: (9...28) = "\\n\\n\\n\\n\\n\\n\\n\\n\\n\n"
-        │   ├── closing_loc: (28...34) = "EOFOO\n"
-        │   └── unescaped: "\n\n\n\n\n\n\n\n\n\n"
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (9...28))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (9...28) = "\\n\\n\\n\\n\\n\\n\\n\\n\\n\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "\n\n\n\n\n\n\n\n\n\n"
+        │   └── closing_loc: (28...34) = "EOFOO\n"
         └── @ ClassNode (location: (35...48))
             ├── locals: []
             ├── class_keyword_loc: (35...40) = "class"
diff --git a/test/yarp/snapshots/seattlerb/parse_line_heredoc_regexp_chars.txt b/test/yarp/snapshots/seattlerb/parse_line_heredoc_regexp_chars.txt
index 44c178e0..8a982418 100644
--- a/test/yarp/snapshots/seattlerb/parse_line_heredoc_regexp_chars.txt
+++ b/test/yarp/snapshots/seattlerb/parse_line_heredoc_regexp_chars.txt
@@ -8,11 +8,15 @@
         │   ├── depth: 0
         │   ├── name_loc: (6...12) = "string"
         │   ├── value:
-        │   │   @ StringNode (location: (15...22))
+        │   │   @ InterpolatedStringNode (location: (15...22))
         │   │   ├── opening_loc: (15...22) = "<<-\"^D\""
-        │   │   ├── content_loc: (23...48) = "        very long string\n"
-        │   │   ├── closing_loc: (48...57) = "      ^D\n"
-        │   │   └── unescaped: "        very long string\n"
+        │   │   ├── parts: (length: 1)
+        │   │   │   └── @ StringNode (location: (23...48))
+        │   │   │       ├── opening_loc: ∅
+        │   │   │       ├── content_loc: (23...48) = "        very long string\n"
+        │   │   │       ├── closing_loc: ∅
+        │   │   │       └── unescaped: "        very long string\n"
+        │   │   └── closing_loc: (48...57) = "      ^D\n"
         │   └── operator_loc: (13...14) = "="
         └── @ CallNode (location: (63...74))
             ├── receiver: ∅
diff --git a/test/yarp/snapshots/seattlerb/pct_w_heredoc_interp_nested.txt b/test/yarp/snapshots/seattlerb/pct_w_heredoc_interp_nested.txt
index 8d0cf0a2..0706a449 100644
--- a/test/yarp/snapshots/seattlerb/pct_w_heredoc_interp_nested.txt
+++ b/test/yarp/snapshots/seattlerb/pct_w_heredoc_interp_nested.txt
@@ -18,11 +18,15 @@
             │   │   │       ├── statements:
             │   │   │       │   @ StatementsNode (location: (8...11))
             │   │   │       │   └── body: (length: 1)
-            │   │   │       │       └── @ StringNode (location: (8...11))
+            │   │   │       │       └── @ InterpolatedStringNode (location: (8...11))
             │   │   │       │           ├── opening_loc: (8...11) = "<<A"
-            │   │   │       │           ├── content_loc: (15...17) = "2\n"
-            │   │   │       │           ├── closing_loc: (17...19) = "A\n"
-            │   │   │       │           └── unescaped: "2\n"
+            │   │   │       │           ├── parts: (length: 1)
+            │   │   │       │           │   └── @ StringNode (location: (15...17))
+            │   │   │       │           │       ├── opening_loc: ∅
+            │   │   │       │           │       ├── content_loc: (15...17) = "2\n"
+            │   │   │       │           │       ├── closing_loc: ∅
+            │   │   │       │           │       └── unescaped: "2\n"
+            │   │   │       │           └── closing_loc: (17...19) = "A\n"
             │   │   │       └── closing_loc: (11...12) = "}"
             │   │   └── closing_loc: ∅
             │   ├── @ StringNode (location: (13...14))
diff --git a/test/yarp/snapshots/single_quote_heredocs.txt b/test/yarp/snapshots/single_quote_heredocs.txt
index d5f5e6f4..c69bdf39 100644
--- a/test/yarp/snapshots/single_quote_heredocs.txt
+++ b/test/yarp/snapshots/single_quote_heredocs.txt
@@ -3,8 +3,12 @@
 └── statements:
     @ StatementsNode (location: (0...8))
     └── body: (length: 1)
-        └── @ StringNode (location: (0...8))
+        └── @ InterpolatedStringNode (location: (0...8))
             ├── opening_loc: (0...8) = "<<-'EOS'"
-            ├── content_loc: (9...44) = "    cd L:\\Work\\MG3710IQPro\\Develop\n"
-            ├── closing_loc: (44...48) = "EOS\n"
-            └── unescaped: "    cd L:\\Work\\MG3710IQPro\\Develop\n"
+            ├── parts: (length: 1)
+            │   └── @ StringNode (location: (9...44))
+            │       ├── opening_loc: ∅
+            │       ├── content_loc: (9...44) = "    cd L:\\Work\\MG3710IQPro\\Develop\n"
+            │       ├── closing_loc: ∅
+            │       └── unescaped: "    cd L:\\Work\\MG3710IQPro\\Develop\n"
+            └── closing_loc: (44...48) = "EOS\n"
diff --git a/test/yarp/snapshots/spanning_heredoc.txt b/test/yarp/snapshots/spanning_heredoc.txt
index edea4efb..9d35b06e 100644
--- a/test/yarp/snapshots/spanning_heredoc.txt
+++ b/test/yarp/snapshots/spanning_heredoc.txt
@@ -13,11 +13,15 @@
         │   │   └── arguments: (length: 1)
         │   │       └── @ CallNode (location: (167...192))
         │   │           ├── receiver:
-        │   │           │   @ StringNode (location: (167...171))
+        │   │           │   @ InterpolatedStringNode (location: (167...171))
         │   │           │   ├── opening_loc: (167...171) = "<<-A"
-        │   │           │   ├── content_loc: (181...183) = "a\n"
-        │   │           │   ├── closing_loc: (183...185) = "A\n"
-        │   │           │   └── unescaped: "a\n"
+        │   │           │   ├── parts: (length: 1)
+        │   │           │   │   └── @ StringNode (location: (181...183))
+        │   │           │   │       ├── opening_loc: ∅
+        │   │           │   │       ├── content_loc: (181...183) = "a\n"
+        │   │           │   │       ├── closing_loc: ∅
+        │   │           │   │       └── unescaped: "a\n"
+        │   │           │   └── closing_loc: (183...185) = "A\n"
         │   │           ├── call_operator_loc: (171...172) = "."
         │   │           ├── message_loc: (172...176) = "gsub"
         │   │           ├── opening_loc: (176...177) = "("
@@ -60,11 +64,15 @@
         │   ├── arguments:
         │   │   @ ArgumentsNode (location: (279...295))
         │   │   └── arguments: (length: 2)
-        │   │       ├── @ StringNode (location: (279...283))
+        │   │       ├── @ InterpolatedStringNode (location: (279...283))
         │   │       │   ├── opening_loc: (279...283) = "<<-A"
-        │   │       │   ├── content_loc: (289...291) = "c\n"
-        │   │       │   ├── closing_loc: (291...293) = "A\n"
-        │   │       │   └── unescaped: "c\n"
+        │   │       │   ├── parts: (length: 1)
+        │   │       │   │   └── @ StringNode (location: (289...291))
+        │   │       │   │       ├── opening_loc: ∅
+        │   │       │   │       ├── content_loc: (289...291) = "c\n"
+        │   │       │   │       ├── closing_loc: ∅
+        │   │       │   │       └── unescaped: "c\n"
+        │   │       │   └── closing_loc: (291...293) = "A\n"
         │   │       └── @ InterpolatedStringNode (location: (285...295))
         │   │           ├── opening_loc: (285...286) = "\""
         │   │           ├── parts: (length: 2)
@@ -91,11 +99,15 @@
         │   ├── arguments:
         │   │   @ ArgumentsNode (location: (325...343))
         │   │   └── arguments: (length: 2)
-        │   │       ├── @ StringNode (location: (325...329))
+        │   │       ├── @ InterpolatedStringNode (location: (325...329))
         │   │       │   ├── opening_loc: (325...329) = "<<-A"
-        │   │       │   ├── content_loc: (337...339) = "e\n"
-        │   │       │   ├── closing_loc: (339...341) = "A\n"
-        │   │       │   └── unescaped: "e\n"
+        │   │       │   ├── parts: (length: 1)
+        │   │       │   │   └── @ StringNode (location: (337...339))
+        │   │       │   │       ├── opening_loc: ∅
+        │   │       │   │       ├── content_loc: (337...339) = "e\n"
+        │   │       │   │       ├── closing_loc: ∅
+        │   │       │   │       └── unescaped: "e\n"
+        │   │       │   └── closing_loc: (339...341) = "A\n"
         │   │       └── @ InterpolatedStringNode (location: (331...343))
         │   │           ├── opening_loc: (331...334) = "%q["
         │   │           ├── parts: (length: 2)
@@ -122,11 +134,15 @@
         │   ├── arguments:
         │   │   @ ArgumentsNode (location: (430...448))
         │   │   └── arguments: (length: 2)
-        │   │       ├── @ StringNode (location: (430...434))
+        │   │       ├── @ InterpolatedStringNode (location: (430...434))
         │   │       │   ├── opening_loc: (430...434) = "<<-A"
-        │   │       │   ├── content_loc: (442...444) = "g\n"
-        │   │       │   ├── closing_loc: (444...446) = "A\n"
-        │   │       │   └── unescaped: "g\n"
+        │   │       │   ├── parts: (length: 1)
+        │   │       │   │   └── @ StringNode (location: (442...444))
+        │   │       │   │       ├── opening_loc: ∅
+        │   │       │   │       ├── content_loc: (442...444) = "g\n"
+        │   │       │   │       ├── closing_loc: ∅
+        │   │       │   │       └── unescaped: "g\n"
+        │   │       │   └── closing_loc: (444...446) = "A\n"
         │   │       └── @ InterpolatedStringNode (location: (436...448))
         │   │           ├── opening_loc: (436...439) = "%Q["
         │   │           ├── parts: (length: 2)
@@ -153,11 +169,15 @@
         │   ├── arguments:
         │   │   @ ArgumentsNode (location: (523...541))
         │   │   └── arguments: (length: 2)
-        │   │       ├── @ StringNode (location: (523...527))
+        │   │       ├── @ InterpolatedStringNode (location: (523...527))
         │   │       │   ├── opening_loc: (523...527) = "<<-A"
-        │   │       │   ├── content_loc: (535...537) = "i\n"
-        │   │       │   ├── closing_loc: (537...539) = "A\n"
-        │   │       │   └── unescaped: "i\n"
+        │   │       │   ├── parts: (length: 1)
+        │   │       │   │   └── @ StringNode (location: (535...537))
+        │   │       │   │       ├── opening_loc: ∅
+        │   │       │   │       ├── content_loc: (535...537) = "i\n"
+        │   │       │   │       ├── closing_loc: ∅
+        │   │       │   │       └── unescaped: "i\n"
+        │   │       │   └── closing_loc: (537...539) = "A\n"
         │   │       └── @ ArrayNode (location: (529...541))
         │   │           ├── elements: (length: 2)
         │   │           │   ├── @ StringNode (location: (532...535))
@@ -184,11 +204,15 @@
         │   ├── arguments:
         │   │   @ ArgumentsNode (location: (691...709))
         │   │   └── arguments: (length: 2)
-        │   │       ├── @ StringNode (location: (691...695))
+        │   │       ├── @ InterpolatedStringNode (location: (691...695))
         │   │       │   ├── opening_loc: (691...695) = "<<-A"
-        │   │       │   ├── content_loc: (703...705) = "k\n"
-        │   │       │   ├── closing_loc: (705...707) = "A\n"
-        │   │       │   └── unescaped: "k\n"
+        │   │       │   ├── parts: (length: 1)
+        │   │       │   │   └── @ StringNode (location: (703...705))
+        │   │       │   │       ├── opening_loc: ∅
+        │   │       │   │       ├── content_loc: (703...705) = "k\n"
+        │   │       │   │       ├── closing_loc: ∅
+        │   │       │   │       └── unescaped: "k\n"
+        │   │       │   └── closing_loc: (705...707) = "A\n"
         │   │       └── @ ArrayNode (location: (697...709))
         │   │           ├── elements: (length: 1)
         │   │           │   └── @ InterpolatedStringNode (location: (700...708))
@@ -219,11 +243,15 @@
         │   ├── arguments:
         │   │   @ ArgumentsNode (location: (784...802))
         │   │   └── arguments: (length: 2)
-        │   │       ├── @ StringNode (location: (784...788))
+        │   │       ├── @ InterpolatedStringNode (location: (784...788))
         │   │       │   ├── opening_loc: (784...788) = "<<-A"
-        │   │       │   ├── content_loc: (796...798) = "m\n"
-        │   │       │   ├── closing_loc: (798...800) = "A\n"
-        │   │       │   └── unescaped: "m\n"
+        │   │       │   ├── parts: (length: 1)
+        │   │       │   │   └── @ StringNode (location: (796...798))
+        │   │       │   │       ├── opening_loc: ∅
+        │   │       │   │       ├── content_loc: (796...798) = "m\n"
+        │   │       │   │       ├── closing_loc: ∅
+        │   │       │   │       └── unescaped: "m\n"
+        │   │       │   └── closing_loc: (798...800) = "A\n"
         │   │       └── @ ArrayNode (location: (790...802))
         │   │           ├── elements: (length: 2)
         │   │           │   ├── @ SymbolNode (location: (793...796))
@@ -250,11 +278,15 @@
             ├── arguments:
             │   @ ArgumentsNode (location: (946...964))
             │   └── arguments: (length: 2)
-            │       ├── @ StringNode (location: (946...950))
+            │       ├── @ InterpolatedStringNode (location: (946...950))
             │       │   ├── opening_loc: (946...950) = "<<-A"
-            │       │   ├── content_loc: (958...960) = "o\n"
-            │       │   ├── closing_loc: (960...962) = "A\n"
-            │       │   └── unescaped: "o\n"
+            │       │   ├── parts: (length: 1)
+            │       │   │   └── @ StringNode (location: (958...960))
+            │       │   │       ├── opening_loc: ∅
+            │       │   │       ├── content_loc: (958...960) = "o\n"
+            │       │   │       ├── closing_loc: ∅
+            │       │   │       └── unescaped: "o\n"
+            │       │   └── closing_loc: (960...962) = "A\n"
             │       └── @ ArrayNode (location: (952...964))
             │           ├── elements: (length: 1)
             │           │   └── @ InterpolatedSymbolNode (location: (955...963))
diff --git a/test/yarp/snapshots/tilde_heredocs.txt b/test/yarp/snapshots/tilde_heredocs.txt
index 3fdb4560..417102ed 100644
--- a/test/yarp/snapshots/tilde_heredocs.txt
+++ b/test/yarp/snapshots/tilde_heredocs.txt
@@ -3,16 +3,24 @@
 └── statements:
     @ StatementsNode (location: (0...372))
     └── body: (length: 18)
-        ├── @ StringNode (location: (0...6))
+        ├── @ InterpolatedStringNode (location: (0...6))
         │   ├── opening_loc: (0...6) = "<<~EOF"
-        │   ├── content_loc: (7...11) = "  a\n"
-        │   ├── closing_loc: (11...15) = "EOF\n"
-        │   └── unescaped: "a\n"
-        ├── @ StringNode (location: (16...22))
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (7...11))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (7...11) = "  a\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "a\n"
+        │   └── closing_loc: (11...15) = "EOF\n"
+        ├── @ InterpolatedStringNode (location: (16...22))
         │   ├── opening_loc: (16...22) = "<<~EOF"
-        │   ├── content_loc: (23...34) = "\ta\n  b\n\t\tc\n"
-        │   ├── closing_loc: (34...38) = "EOF\n"
-        │   └── unescaped: "\ta\nb\n\t\tc\n"
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (23...34))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (23...34) = "\ta\n  b\n\t\tc\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "\ta\nb\n\t\tc\n"
+        │   └── closing_loc: (34...38) = "EOF\n"
         ├── @ InterpolatedStringNode (location: (39...45))
         │   ├── opening_loc: (39...45) = "<<~EOF"
         │   ├── parts: (length: 2)
@@ -96,56 +104,96 @@
         │   │       ├── closing_loc: ∅
         │   │       └── unescaped: "\n"
         │   └── closing_loc: (121...125) = "EOF\n"
-        ├── @ StringNode (location: (126...132))
+        ├── @ InterpolatedStringNode (location: (126...132))
         │   ├── opening_loc: (126...132) = "<<~EOF"
-        │   ├── content_loc: (133...141) = "  a\n  b\n"
-        │   ├── closing_loc: (141...145) = "EOF\n"
-        │   └── unescaped: "a\nb\n"
-        ├── @ StringNode (location: (146...152))
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (133...141))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (133...141) = "  a\n  b\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "a\nb\n"
+        │   └── closing_loc: (141...145) = "EOF\n"
+        ├── @ InterpolatedStringNode (location: (146...152))
         │   ├── opening_loc: (146...152) = "<<~EOF"
-        │   ├── content_loc: (153...162) = "  a\n   b\n"
-        │   ├── closing_loc: (162...166) = "EOF\n"
-        │   └── unescaped: "a\n b\n"
-        ├── @ StringNode (location: (167...173))
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (153...162))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (153...162) = "  a\n   b\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "a\n b\n"
+        │   └── closing_loc: (162...166) = "EOF\n"
+        ├── @ InterpolatedStringNode (location: (167...173))
         │   ├── opening_loc: (167...173) = "<<~EOF"
-        │   ├── content_loc: (174...183) = "\t\t\ta\n\t\tb\n"
-        │   ├── closing_loc: (183...187) = "EOF\n"
-        │   └── unescaped: "\ta\nb\n"
-        ├── @ StringNode (location: (188...196))
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (174...183))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (174...183) = "\t\t\ta\n\t\tb\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "\ta\nb\n"
+        │   └── closing_loc: (183...187) = "EOF\n"
+        ├── @ InterpolatedStringNode (location: (188...196))
         │   ├── opening_loc: (188...196) = "<<~'EOF'"
-        │   ├── content_loc: (197...206) = "  a \#{1}\n"
-        │   ├── closing_loc: (206...210) = "EOF\n"
-        │   └── unescaped: "a \#{1}\n"
-        ├── @ StringNode (location: (211...217))
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (197...206))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (197...206) = "  a \#{1}\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "a \#{1}\n"
+        │   └── closing_loc: (206...210) = "EOF\n"
+        ├── @ InterpolatedStringNode (location: (211...217))
         │   ├── opening_loc: (211...217) = "<<~EOF"
-        │   ├── content_loc: (218...225) = "\ta\n\t b\n"
-        │   ├── closing_loc: (225...229) = "EOF\n"
-        │   └── unescaped: "a\n b\n"
-        ├── @ StringNode (location: (230...236))
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (218...225))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (218...225) = "\ta\n\t b\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "a\n b\n"
+        │   └── closing_loc: (225...229) = "EOF\n"
+        ├── @ InterpolatedStringNode (location: (230...236))
         │   ├── opening_loc: (230...236) = "<<~EOF"
-        │   ├── content_loc: (237...244) = "\t a\n\tb\n"
-        │   ├── closing_loc: (244...248) = "EOF\n"
-        │   └── unescaped: " a\nb\n"
-        ├── @ StringNode (location: (249...255))
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (237...244))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (237...244) = "\t a\n\tb\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: " a\nb\n"
+        │   └── closing_loc: (244...248) = "EOF\n"
+        ├── @ InterpolatedStringNode (location: (249...255))
         │   ├── opening_loc: (249...255) = "<<~EOF"
-        │   ├── content_loc: (256...271) = "  \ta\n        b\n"
-        │   ├── closing_loc: (271...275) = "EOF\n"
-        │   └── unescaped: "a\nb\n"
-        ├── @ StringNode (location: (276...282))
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (256...271))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (256...271) = "  \ta\n        b\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "a\nb\n"
+        │   └── closing_loc: (271...275) = "EOF\n"
+        ├── @ InterpolatedStringNode (location: (276...282))
         │   ├── opening_loc: (276...282) = "<<~EOF"
-        │   ├── content_loc: (283...292) = "  a\n\n  b\n"
-        │   ├── closing_loc: (292...296) = "EOF\n"
-        │   └── unescaped: "a\n\nb\n"
-        ├── @ StringNode (location: (297...303))
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (283...292))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (283...292) = "  a\n\n  b\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "a\n\nb\n"
+        │   └── closing_loc: (292...296) = "EOF\n"
+        ├── @ InterpolatedStringNode (location: (297...303))
         │   ├── opening_loc: (297...303) = "<<~EOF"
-        │   ├── content_loc: (304...313) = "  a\n\n  b\n"
-        │   ├── closing_loc: (313...317) = "EOF\n"
-        │   └── unescaped: "a\n\nb\n"
-        ├── @ StringNode (location: (318...324))
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (304...313))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (304...313) = "  a\n\n  b\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "a\n\nb\n"
+        │   └── closing_loc: (313...317) = "EOF\n"
+        ├── @ InterpolatedStringNode (location: (318...324))
         │   ├── opening_loc: (318...324) = "<<~EOF"
-        │   ├── content_loc: (325...336) = "  a\n\n\n\n  b\n"
-        │   ├── closing_loc: (336...340) = "EOF\n"
-        │   └── unescaped: "a\n\n\n\nb\n"
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (325...336))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (325...336) = "  a\n\n\n\n  b\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "a\n\n\n\nb\n"
+        │   └── closing_loc: (336...340) = "EOF\n"
         ├── @ InterpolatedStringNode (location: (341...347))
         │   ├── opening_loc: (341...347) = "<<~EOF"
         │   ├── parts: (length: 3)
diff --git a/test/yarp/snapshots/unescaping.txt b/test/yarp/snapshots/unescaping.txt
index 90c835d8..d0208ca2 100644
--- a/test/yarp/snapshots/unescaping.txt
+++ b/test/yarp/snapshots/unescaping.txt
@@ -23,8 +23,12 @@
         │   ├── content_loc: (23...29) = "\\c\#{1}"
         │   ├── closing_loc: (29...30) = "\""
         │   └── unescaped: "\u0003{1}"
-        └── @ StringNode (location: (32...39))
+        └── @ InterpolatedStringNode (location: (32...39))
             ├── opening_loc: (32...39) = "<<~HERE"
-            ├── content_loc: (40...50) = "   \\c\#{1}\n"
-            ├── closing_loc: (50...55) = "HERE\n"
-            └── unescaped: "\u0003{1}\n"
+            ├── parts: (length: 1)
+            │   └── @ StringNode (location: (40...50))
+            │       ├── opening_loc: ∅
+            │       ├── content_loc: (40...50) = "   \\c\#{1}\n"
+            │       ├── closing_loc: ∅
+            │       └── unescaped: "\u0003{1}\n"
+            └── closing_loc: (50...55) = "HERE\n"
diff --git a/test/yarp/snapshots/unparser/corpus/semantic/block.txt b/test/yarp/snapshots/unparser/corpus/semantic/block.txt
index 1f8675f1..4a16c239 100644
--- a/test/yarp/snapshots/unparser/corpus/semantic/block.txt
+++ b/test/yarp/snapshots/unparser/corpus/semantic/block.txt
@@ -113,11 +113,15 @@
         │   ├── arguments:
         │   │   @ ArgumentsNode (location: (86...92))
         │   │   └── arguments: (length: 1)
-        │   │       └── @ StringNode (location: (86...92))
+        │   │       └── @ InterpolatedStringNode (location: (86...92))
         │   │           ├── opening_loc: (86...92) = "<<-DOC"
-        │   │           ├── content_loc: (101...105) = "  b\n"
-        │   │           ├── closing_loc: (105...109) = "DOC\n"
-        │   │           └── unescaped: "  b\n"
+        │   │           ├── parts: (length: 1)
+        │   │           │   └── @ StringNode (location: (101...105))
+        │   │           │       ├── opening_loc: ∅
+        │   │           │       ├── content_loc: (101...105) = "  b\n"
+        │   │           │       ├── closing_loc: ∅
+        │   │           │       └── unescaped: "  b\n"
+        │   │           └── closing_loc: (105...109) = "DOC\n"
         │   ├── closing_loc: (92...93) = ")"
         │   ├── block:
         │   │   @ BlockNode (location: (94...116))
@@ -156,11 +160,15 @@
             ├── arguments:
             │   @ ArgumentsNode (location: (122...128))
             │   └── arguments: (length: 1)
-            │       └── @ StringNode (location: (122...128))
+            │       └── @ InterpolatedStringNode (location: (122...128))
             │           ├── opening_loc: (122...128) = "<<-DOC"
-            │           ├── content_loc: (133...137) = "  b\n"
-            │           ├── closing_loc: (137...141) = "DOC\n"
-            │           └── unescaped: "  b\n"
+            │           ├── parts: (length: 1)
+            │           │   └── @ StringNode (location: (133...137))
+            │           │       ├── opening_loc: ∅
+            │           │       ├── content_loc: (133...137) = "  b\n"
+            │           │       ├── closing_loc: ∅
+            │           │       └── unescaped: "  b\n"
+            │           └── closing_loc: (137...141) = "DOC\n"
             ├── closing_loc: (128...129) = ")"
             ├── block:
             │   @ BlockNode (location: (130...148))
diff --git a/test/yarp/snapshots/unparser/corpus/semantic/dstr.txt b/test/yarp/snapshots/unparser/corpus/semantic/dstr.txt
index 55349513..080b3ab8 100644
--- a/test/yarp/snapshots/unparser/corpus/semantic/dstr.txt
+++ b/test/yarp/snapshots/unparser/corpus/semantic/dstr.txt
@@ -3,36 +3,40 @@
 └── statements:
     @ StatementsNode (location: (0...608))
     └── body: (length: 33)
-        ├── @ StringNode (location: (0...5))
+        ├── @ InterpolatedStringNode (location: (0...5))
         │   ├── opening_loc: (0...5) = "<<DOC"
-        │   ├── content_loc: (5...6) = "\n"
-        │   ├── closing_loc: (6...10) = "DOC\n"
-        │   └── unescaped: ""
-        ├── @ StringNode (location: (11...18))
+        │   ├── parts: (length: 0)
+        │   └── closing_loc: (6...10) = "DOC\n"
+        ├── @ InterpolatedStringNode (location: (11...18))
         │   ├── opening_loc: (11...18) = "<<'DOC'"
-        │   ├── content_loc: (18...19) = "\n"
-        │   ├── closing_loc: (19...23) = "DOC\n"
-        │   └── unescaped: ""
-        ├── @ StringNode (location: (24...30))
+        │   ├── parts: (length: 0)
+        │   └── closing_loc: (19...23) = "DOC\n"
+        ├── @ InterpolatedStringNode (location: (24...30))
         │   ├── opening_loc: (24...30) = "<<~DOC"
-        │   ├── content_loc: (30...31) = "\n"
-        │   ├── closing_loc: (31...35) = "DOC\n"
-        │   └── unescaped: ""
-        ├── @ StringNode (location: (36...44))
+        │   ├── parts: (length: 0)
+        │   └── closing_loc: (31...35) = "DOC\n"
+        ├── @ InterpolatedStringNode (location: (36...44))
         │   ├── opening_loc: (36...44) = "<<~'DOC'"
-        │   ├── content_loc: (44...45) = "\n"
-        │   ├── closing_loc: (45...49) = "DOC\n"
-        │   └── unescaped: ""
-        ├── @ StringNode (location: (50...55))
+        │   ├── parts: (length: 0)
+        │   └── closing_loc: (45...49) = "DOC\n"
+        ├── @ InterpolatedStringNode (location: (50...55))
         │   ├── opening_loc: (50...55) = "<<DOC"
-        │   ├── content_loc: (56...60) = "  a\n"
-        │   ├── closing_loc: (60...64) = "DOC\n"
-        │   └── unescaped: "  a\n"
-        ├── @ StringNode (location: (65...72))
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (56...60))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (56...60) = "  a\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "  a\n"
+        │   └── closing_loc: (60...64) = "DOC\n"
+        ├── @ InterpolatedStringNode (location: (65...72))
         │   ├── opening_loc: (65...72) = "<<'DOC'"
-        │   ├── content_loc: (73...77) = "  a\n"
-        │   ├── closing_loc: (77...81) = "DOC\n"
-        │   └── unescaped: "  a\n"
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (73...77))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (73...77) = "  a\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "  a\n"
+        │   └── closing_loc: (77...81) = "DOC\n"
         ├── @ InterpolatedStringNode (location: (82...87))
         │   ├── opening_loc: (82...87) = "<<DOC"
         │   ├── parts: (length: 3)
@@ -87,26 +91,42 @@
         │   │       ├── closing_loc: ∅
         │   │       └── unescaped: "\nb\n"
         │   └── closing_loc: (146...150) = "DOC\n"
-        ├── @ StringNode (location: (151...157))
+        ├── @ InterpolatedStringNode (location: (151...157))
         │   ├── opening_loc: (151...157) = "<<~DOC"
-        │   ├── content_loc: (158...168) = "  a\n    b\n"
-        │   ├── closing_loc: (168...172) = "DOC\n"
-        │   └── unescaped: "a\n  b\n"
-        ├── @ StringNode (location: (173...180))
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (158...168))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (158...168) = "  a\n    b\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "a\n  b\n"
+        │   └── closing_loc: (168...172) = "DOC\n"
+        ├── @ InterpolatedStringNode (location: (173...180))
         │   ├── opening_loc: (173...180) = "<<'DOC'"
-        │   ├── content_loc: (181...186) = "a\n\nb\n"
-        │   ├── closing_loc: (186...190) = "DOC\n"
-        │   └── unescaped: "a\n\nb\n"
-        ├── @ StringNode (location: (191...198))
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (181...186))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (181...186) = "a\n\nb\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "a\n\nb\n"
+        │   └── closing_loc: (186...190) = "DOC\n"
+        ├── @ InterpolatedStringNode (location: (191...198))
         │   ├── opening_loc: (191...198) = "<<'DOC'"
-        │   ├── content_loc: (199...206) = " a\n\n b\n"
-        │   ├── closing_loc: (206...210) = "DOC\n"
-        │   └── unescaped: " a\n\n b\n"
-        ├── @ StringNode (location: (211...218))
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (199...206))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (199...206) = " a\n\n b\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: " a\n\n b\n"
+        │   └── closing_loc: (206...210) = "DOC\n"
+        ├── @ InterpolatedStringNode (location: (211...218))
         │   ├── opening_loc: (211...218) = "<<'DOC'"
-        │   ├── content_loc: (219...225) = " a\\nb\n"
-        │   ├── closing_loc: (225...229) = "DOC\n"
-        │   └── unescaped: " a\\nb\n"
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (219...225))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (219...225) = " a\\nb\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: " a\\nb\n"
+        │   └── closing_loc: (225...229) = "DOC\n"
         ├── @ InterpolatedStringNode (location: (230...235))
         │   ├── opening_loc: (230...235) = "<<DOC"
         │   ├── parts: (length: 4)
@@ -256,11 +276,15 @@
         │   ├── statements:
         │   │   @ StatementsNode (location: (435...443))
         │   │   └── body: (length: 1)
-        │   │       └── @ StringNode (location: (435...443))
+        │   │       └── @ InterpolatedStringNode (location: (435...443))
         │   │           ├── opening_loc: (435...443) = "<<-'DOC'"
-        │   │           ├── content_loc: (444...455) = "   a\n\n   b\n"
-        │   │           ├── closing_loc: (455...461) = "  DOC\n"
-        │   │           └── unescaped: "   a\n\n   b\n"
+        │   │           ├── parts: (length: 1)
+        │   │           │   └── @ StringNode (location: (444...455))
+        │   │           │       ├── opening_loc: ∅
+        │   │           │       ├── content_loc: (444...455) = "   a\n\n   b\n"
+        │   │           │       ├── closing_loc: ∅
+        │   │           │       └── unescaped: "   a\n\n   b\n"
+        │   │           └── closing_loc: (455...461) = "  DOC\n"
         │   ├── consequent: ∅
         │   └── end_keyword_loc: (461...464) = "end"
         ├── @ InterpolatedStringNode (location: (466...472))
diff --git a/test/yarp/snapshots/unparser/corpus/semantic/while.txt b/test/yarp/snapshots/unparser/corpus/semantic/while.txt
index b702e7fc..323528a0 100644
--- a/test/yarp/snapshots/unparser/corpus/semantic/while.txt
+++ b/test/yarp/snapshots/unparser/corpus/semantic/while.txt
@@ -185,11 +185,10 @@
         │   │   ├── arguments:
         │   │   │   @ ArgumentsNode (location: (108...114))
         │   │   │   └── arguments: (length: 1)
-        │   │   │       └── @ StringNode (location: (108...114))
+        │   │   │       └── @ InterpolatedStringNode (location: (108...114))
         │   │   │           ├── opening_loc: (108...114) = "<<-FOO"
-        │   │   │           ├── content_loc: (114...119) = ") do\n"
-        │   │   │           ├── closing_loc: (119...123) = "FOO\n"
-        │   │   │           └── unescaped: ""
+        │   │   │           ├── parts: (length: 0)
+        │   │   │           └── closing_loc: (119...123) = "FOO\n"
         │   │   ├── closing_loc: (114...115) = ")"
         │   │   ├── block:
         │   │   │   @ BlockNode (location: (116...130))
diff --git a/test/yarp/snapshots/whitequark/bug_heredoc_do.txt b/test/yarp/snapshots/whitequark/bug_heredoc_do.txt
index 34cb4b2b..6707aa21 100644
--- a/test/yarp/snapshots/whitequark/bug_heredoc_do.txt
+++ b/test/yarp/snapshots/whitequark/bug_heredoc_do.txt
@@ -11,11 +11,10 @@
             ├── arguments:
             │   @ ArgumentsNode (location: (2...10))
             │   └── arguments: (length: 1)
-            │       └── @ StringNode (location: (2...10))
+            │       └── @ InterpolatedStringNode (location: (2...10))
             │           ├── opening_loc: (2...10) = "<<-TABLE"
-            │           ├── content_loc: (10...14) = " do\n"
-            │           ├── closing_loc: (14...20) = "TABLE\n"
-            │           └── unescaped: ""
+            │           ├── parts: (length: 0)
+            │           └── closing_loc: (14...20) = "TABLE\n"
             ├── closing_loc: ∅
             ├── block:
             │   @ BlockNode (location: (11...23))
diff --git a/test/yarp/snapshots/whitequark/dedenting_heredoc.txt b/test/yarp/snapshots/whitequark/dedenting_heredoc.txt
index 564a8fa9..a0a900f6 100644
--- a/test/yarp/snapshots/whitequark/dedenting_heredoc.txt
+++ b/test/yarp/snapshots/whitequark/dedenting_heredoc.txt
@@ -90,11 +90,15 @@
         │   ├── arguments:
         │   │   @ ArgumentsNode (location: (58...62))
         │   │   └── arguments: (length: 1)
-        │   │       └── @ StringNode (location: (58...62))
+        │   │       └── @ InterpolatedStringNode (location: (58...62))
         │   │           ├── opening_loc: (58...62) = "<<~E"
-        │   │           ├── content_loc: (63...76) = "\tx\n        y\n"
-        │   │           ├── closing_loc: (76...78) = "E\n"
-        │   │           └── unescaped: "x\ny\n"
+        │   │           ├── parts: (length: 1)
+        │   │           │   └── @ StringNode (location: (63...76))
+        │   │           │       ├── opening_loc: ∅
+        │   │           │       ├── content_loc: (63...76) = "\tx\n        y\n"
+        │   │           │       ├── closing_loc: ∅
+        │   │           │       └── unescaped: "x\ny\n"
+        │   │           └── closing_loc: (76...78) = "E\n"
         │   ├── closing_loc: ∅
         │   ├── block: ∅
         │   ├── flags: 
@@ -107,11 +111,15 @@
         │   ├── arguments:
         │   │   @ ArgumentsNode (location: (81...85))
         │   │   └── arguments: (length: 1)
-        │   │       └── @ StringNode (location: (81...85))
+        │   │       └── @ InterpolatedStringNode (location: (81...85))
         │   │           ├── opening_loc: (81...85) = "<<~E"
-        │   │           ├── content_loc: (86...95) = "\tx\n    y\n"
-        │   │           ├── closing_loc: (95...97) = "E\n"
-        │   │           └── unescaped: "\tx\ny\n"
+        │   │           ├── parts: (length: 1)
+        │   │           │   └── @ StringNode (location: (86...95))
+        │   │           │       ├── opening_loc: ∅
+        │   │           │       ├── content_loc: (86...95) = "\tx\n    y\n"
+        │   │           │       ├── closing_loc: ∅
+        │   │           │       └── unescaped: "\tx\ny\n"
+        │   │           └── closing_loc: (95...97) = "E\n"
         │   ├── closing_loc: ∅
         │   ├── block: ∅
         │   ├── flags: 
@@ -124,11 +132,15 @@
         │   ├── arguments:
         │   │   @ ArgumentsNode (location: (100...104))
         │   │   └── arguments: (length: 1)
-        │   │       └── @ StringNode (location: (100...104))
+        │   │       └── @ InterpolatedStringNode (location: (100...104))
         │   │           ├── opening_loc: (100...104) = "<<~E"
-        │   │           ├── content_loc: (105...122) = "    \tx\n        y\n"
-        │   │           ├── closing_loc: (122...124) = "E\n"
-        │   │           └── unescaped: "x\ny\n"
+        │   │           ├── parts: (length: 1)
+        │   │           │   └── @ StringNode (location: (105...122))
+        │   │           │       ├── opening_loc: ∅
+        │   │           │       ├── content_loc: (105...122) = "    \tx\n        y\n"
+        │   │           │       ├── closing_loc: ∅
+        │   │           │       └── unescaped: "x\ny\n"
+        │   │           └── closing_loc: (122...124) = "E\n"
         │   ├── closing_loc: ∅
         │   ├── block: ∅
         │   ├── flags: 
@@ -141,11 +153,15 @@
         │   ├── arguments:
         │   │   @ ArgumentsNode (location: (127...131))
         │   │   └── arguments: (length: 1)
-        │   │       └── @ StringNode (location: (127...131))
+        │   │       └── @ InterpolatedStringNode (location: (127...131))
         │   │           ├── opening_loc: (127...131) = "<<~E"
-        │   │           ├── content_loc: (132...146) = "        \tx\n\ty\n"
-        │   │           ├── closing_loc: (146...148) = "E\n"
-        │   │           └── unescaped: "\tx\ny\n"
+        │   │           ├── parts: (length: 1)
+        │   │           │   └── @ StringNode (location: (132...146))
+        │   │           │       ├── opening_loc: ∅
+        │   │           │       ├── content_loc: (132...146) = "        \tx\n\ty\n"
+        │   │           │       ├── closing_loc: ∅
+        │   │           │       └── unescaped: "\tx\ny\n"
+        │   │           └── closing_loc: (146...148) = "E\n"
         │   ├── closing_loc: ∅
         │   ├── block: ∅
         │   ├── flags: 
@@ -158,11 +174,15 @@
         │   ├── arguments:
         │   │   @ ArgumentsNode (location: (151...155))
         │   │   └── arguments: (length: 1)
-        │   │       └── @ StringNode (location: (151...155))
+        │   │       └── @ InterpolatedStringNode (location: (151...155))
         │   │           ├── opening_loc: (151...155) = "<<~E"
-        │   │           ├── content_loc: (156...168) = "    x\n  \\\ty\n"
-        │   │           ├── closing_loc: (168...170) = "E\n"
-        │   │           └── unescaped: "  x\n\ty\n"
+        │   │           ├── parts: (length: 1)
+        │   │           │   └── @ StringNode (location: (156...168))
+        │   │           │       ├── opening_loc: ∅
+        │   │           │       ├── content_loc: (156...168) = "    x\n  \\\ty\n"
+        │   │           │       ├── closing_loc: ∅
+        │   │           │       └── unescaped: "  x\n\ty\n"
+        │   │           └── closing_loc: (168...170) = "E\n"
         │   ├── closing_loc: ∅
         │   ├── block: ∅
         │   ├── flags: 
@@ -175,11 +195,15 @@
         │   ├── arguments:
         │   │   @ ArgumentsNode (location: (173...177))
         │   │   └── arguments: (length: 1)
-        │   │       └── @ StringNode (location: (173...177))
+        │   │       └── @ InterpolatedStringNode (location: (173...177))
         │   │           ├── opening_loc: (173...177) = "<<~E"
-        │   │           ├── content_loc: (178...191) = "    x\n  \\  y\n"
-        │   │           ├── closing_loc: (191...193) = "E\n"
-        │   │           └── unescaped: "  x\n  y\n"
+        │   │           ├── parts: (length: 1)
+        │   │           │   └── @ StringNode (location: (178...191))
+        │   │           │       ├── opening_loc: ∅
+        │   │           │       ├── content_loc: (178...191) = "    x\n  \\  y\n"
+        │   │           │       ├── closing_loc: ∅
+        │   │           │       └── unescaped: "  x\n  y\n"
+        │   │           └── closing_loc: (191...193) = "E\n"
         │   ├── closing_loc: ∅
         │   ├── block: ∅
         │   ├── flags: 
@@ -192,11 +216,10 @@
         │   ├── arguments:
         │   │   @ ArgumentsNode (location: (196...200))
         │   │   └── arguments: (length: 1)
-        │   │       └── @ StringNode (location: (196...200))
+        │   │       └── @ InterpolatedStringNode (location: (196...200))
         │   │           ├── opening_loc: (196...200) = "<<~E"
-        │   │           ├── content_loc: (200...201) = "\n"
-        │   │           ├── closing_loc: (201...205) = "  E\n"
-        │   │           └── unescaped: ""
+        │   │           ├── parts: (length: 0)
+        │   │           └── closing_loc: (201...205) = "  E\n"
         │   ├── closing_loc: ∅
         │   ├── block: ∅
         │   ├── flags: 
@@ -209,11 +232,15 @@
         │   ├── arguments:
         │   │   @ ArgumentsNode (location: (208...212))
         │   │   └── arguments: (length: 1)
-        │   │       └── @ StringNode (location: (208...212))
+        │   │       └── @ InterpolatedStringNode (location: (208...212))
         │   │           ├── opening_loc: (208...212) = "<<~E"
-        │   │           ├── content_loc: (213...220) = "  x\n\ny\n"
-        │   │           ├── closing_loc: (220...222) = "E\n"
-        │   │           └── unescaped: "  x\n\ny\n"
+        │   │           ├── parts: (length: 1)
+        │   │           │   └── @ StringNode (location: (213...220))
+        │   │           │       ├── opening_loc: ∅
+        │   │           │       ├── content_loc: (213...220) = "  x\n\ny\n"
+        │   │           │       ├── closing_loc: ∅
+        │   │           │       └── unescaped: "  x\n\ny\n"
+        │   │           └── closing_loc: (220...222) = "E\n"
         │   ├── closing_loc: ∅
         │   ├── block: ∅
         │   ├── flags: 
@@ -226,11 +253,15 @@
         │   ├── arguments:
         │   │   @ ArgumentsNode (location: (225...229))
         │   │   └── arguments: (length: 1)
-        │   │       └── @ StringNode (location: (225...229))
+        │   │       └── @ InterpolatedStringNode (location: (225...229))
         │   │           ├── opening_loc: (225...229) = "<<~E"
-        │   │           ├── content_loc: (230...243) = "  x\n    \n  y\n"
-        │   │           ├── closing_loc: (243...245) = "E\n"
-        │   │           └── unescaped: "x\n  \ny\n"
+        │   │           ├── parts: (length: 1)
+        │   │           │   └── @ StringNode (location: (230...243))
+        │   │           │       ├── opening_loc: ∅
+        │   │           │       ├── content_loc: (230...243) = "  x\n    \n  y\n"
+        │   │           │       ├── closing_loc: ∅
+        │   │           │       └── unescaped: "x\n  \ny\n"
+        │   │           └── closing_loc: (243...245) = "E\n"
         │   ├── closing_loc: ∅
         │   ├── block: ∅
         │   ├── flags: 
@@ -243,11 +274,15 @@
         │   ├── arguments:
         │   │   @ ArgumentsNode (location: (248...252))
         │   │   └── arguments: (length: 1)
-        │   │       └── @ StringNode (location: (248...252))
+        │   │       └── @ InterpolatedStringNode (location: (248...252))
         │   │           ├── opening_loc: (248...252) = "<<~E"
-        │   │           ├── content_loc: (253...263) = "  x\n    y\n"
-        │   │           ├── closing_loc: (263...265) = "E\n"
-        │   │           └── unescaped: "x\n  y\n"
+        │   │           ├── parts: (length: 1)
+        │   │           │   └── @ StringNode (location: (253...263))
+        │   │           │       ├── opening_loc: ∅
+        │   │           │       ├── content_loc: (253...263) = "  x\n    y\n"
+        │   │           │       ├── closing_loc: ∅
+        │   │           │       └── unescaped: "x\n  y\n"
+        │   │           └── closing_loc: (263...265) = "E\n"
         │   ├── closing_loc: ∅
         │   ├── block: ∅
         │   ├── flags: 
@@ -260,11 +295,15 @@
         │   ├── arguments:
         │   │   @ ArgumentsNode (location: (268...272))
         │   │   └── arguments: (length: 1)
-        │   │       └── @ StringNode (location: (268...272))
+        │   │       └── @ InterpolatedStringNode (location: (268...272))
         │   │           ├── opening_loc: (268...272) = "<<~E"
-        │   │           ├── content_loc: (273...277) = "  x\n"
-        │   │           ├── closing_loc: (277...279) = "E\n"
-        │   │           └── unescaped: "x\n"
+        │   │           ├── parts: (length: 1)
+        │   │           │   └── @ StringNode (location: (273...277))
+        │   │           │       ├── opening_loc: ∅
+        │   │           │       ├── content_loc: (273...277) = "  x\n"
+        │   │           │       ├── closing_loc: ∅
+        │   │           │       └── unescaped: "x\n"
+        │   │           └── closing_loc: (277...279) = "E\n"
         │   ├── closing_loc: ∅
         │   ├── block: ∅
         │   ├── flags: 
@@ -277,11 +316,15 @@
         │   ├── arguments:
         │   │   @ ArgumentsNode (location: (282...286))
         │   │   └── arguments: (length: 1)
-        │   │       └── @ StringNode (location: (282...286))
+        │   │       └── @ InterpolatedStringNode (location: (282...286))
         │   │           ├── opening_loc: (282...286) = "<<~E"
-        │   │           ├── content_loc: (287...292) = "  ð\n"
-        │   │           ├── closing_loc: (292...294) = "E\n"
-        │   │           └── unescaped: "ð\n"
+        │   │           ├── parts: (length: 1)
+        │   │           │   └── @ StringNode (location: (287...292))
+        │   │           │       ├── opening_loc: ∅
+        │   │           │       ├── content_loc: (287...292) = "  ð\n"
+        │   │           │       ├── closing_loc: ∅
+        │   │           │       └── unescaped: "ð\n"
+        │   │           └── closing_loc: (292...294) = "E\n"
         │   ├── closing_loc: ∅
         │   ├── block: ∅
         │   ├── flags: 
@@ -294,11 +337,10 @@
         │   ├── arguments:
         │   │   @ ArgumentsNode (location: (297...301))
         │   │   └── arguments: (length: 1)
-        │   │       └── @ StringNode (location: (297...301))
+        │   │       └── @ InterpolatedStringNode (location: (297...301))
         │   │           ├── opening_loc: (297...301) = "<<~E"
-        │   │           ├── content_loc: (301...302) = "\n"
-        │   │           ├── closing_loc: (302...304) = "E\n"
-        │   │           └── unescaped: ""
+        │   │           ├── parts: (length: 0)
+        │   │           └── closing_loc: (302...304) = "E\n"
         │   ├── closing_loc: ∅
         │   ├── block: ∅
         │   ├── flags: 
diff --git a/test/yarp/snapshots/whitequark/dedenting_interpolating_heredoc_fake_line_continuation.txt b/test/yarp/snapshots/whitequark/dedenting_interpolating_heredoc_fake_line_continuation.txt
index 6ee986bd..fb23aed9 100644
--- a/test/yarp/snapshots/whitequark/dedenting_interpolating_heredoc_fake_line_continuation.txt
+++ b/test/yarp/snapshots/whitequark/dedenting_interpolating_heredoc_fake_line_continuation.txt
@@ -3,8 +3,12 @@
 └── statements:
     @ StatementsNode (location: (0...8))
     └── body: (length: 1)
-        └── @ StringNode (location: (0...8))
+        └── @ InterpolatedStringNode (location: (0...8))
             ├── opening_loc: (0...8) = "<<~'FOO'"
-            ├── content_loc: (9...23) = "  baz\\\\\n  qux\n"
-            ├── closing_loc: (23...27) = "FOO\n"
-            └── unescaped: "baz\\\nqux\n"
+            ├── parts: (length: 1)
+            │   └── @ StringNode (location: (9...23))
+            │       ├── opening_loc: ∅
+            │       ├── content_loc: (9...23) = "  baz\\\\\n  qux\n"
+            │       ├── closing_loc: ∅
+            │       └── unescaped: "baz\\\nqux\n"
+            └── closing_loc: (23...27) = "FOO\n"
diff --git a/test/yarp/snapshots/whitequark/dedenting_non_interpolating_heredoc_line_continuation.txt b/test/yarp/snapshots/whitequark/dedenting_non_interpolating_heredoc_line_continuation.txt
index b84db0a0..4f349a26 100644
--- a/test/yarp/snapshots/whitequark/dedenting_non_interpolating_heredoc_line_continuation.txt
+++ b/test/yarp/snapshots/whitequark/dedenting_non_interpolating_heredoc_line_continuation.txt
@@ -3,8 +3,12 @@
 └── statements:
     @ StatementsNode (location: (0...8))
     └── body: (length: 1)
-        └── @ StringNode (location: (0...8))
+        └── @ InterpolatedStringNode (location: (0...8))
             ├── opening_loc: (0...8) = "<<~'FOO'"
-            ├── content_loc: (9...22) = "  baz\\\n  qux\n"
-            ├── closing_loc: (22...26) = "FOO\n"
-            └── unescaped: "baz\\\nqux\n"
+            ├── parts: (length: 1)
+            │   └── @ StringNode (location: (9...22))
+            │       ├── opening_loc: ∅
+            │       ├── content_loc: (9...22) = "  baz\\\n  qux\n"
+            │       ├── closing_loc: ∅
+            │       └── unescaped: "baz\\\nqux\n"
+            └── closing_loc: (22...26) = "FOO\n"
diff --git a/test/yarp/snapshots/whitequark/heredoc.txt b/test/yarp/snapshots/whitequark/heredoc.txt
index 1d62a96c..082e2392 100644
--- a/test/yarp/snapshots/whitequark/heredoc.txt
+++ b/test/yarp/snapshots/whitequark/heredoc.txt
@@ -3,18 +3,30 @@
 └── statements:
     @ StatementsNode (location: (0...52))
     └── body: (length: 3)
-        ├── @ StringNode (location: (0...8))
+        ├── @ InterpolatedStringNode (location: (0...8))
         │   ├── opening_loc: (0...8) = "<<'HERE'"
-        │   ├── content_loc: (9...17) = "foo\nbar\n"
-        │   ├── closing_loc: (17...22) = "HERE\n"
-        │   └── unescaped: "foo\nbar\n"
-        ├── @ StringNode (location: (23...29))
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (9...17))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (9...17) = "foo\nbar\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "foo\nbar\n"
+        │   └── closing_loc: (17...22) = "HERE\n"
+        ├── @ InterpolatedStringNode (location: (23...29))
         │   ├── opening_loc: (23...29) = "<<HERE"
-        │   ├── content_loc: (30...38) = "foo\nbar\n"
-        │   ├── closing_loc: (38...43) = "HERE\n"
-        │   └── unescaped: "foo\nbar\n"
-        └── @ XStringNode (location: (44...52))
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (30...38))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (30...38) = "foo\nbar\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "foo\nbar\n"
+        │   └── closing_loc: (38...43) = "HERE\n"
+        └── @ InterpolatedXStringNode (location: (44...52))
             ├── opening_loc: (44...52) = "<<`HERE`"
-            ├── content_loc: (53...61) = "foo\nbar\n"
-            ├── closing_loc: (61...66) = "HERE\n"
-            └── unescaped: "foo\nbar\n"
+            ├── parts: (length: 1)
+            │   └── @ StringNode (location: (53...61))
+            │       ├── opening_loc: ∅
+            │       ├── content_loc: (53...61) = "foo\nbar\n"
+            │       ├── closing_loc: ∅
+            │       └── unescaped: "foo\nbar\n"
+            └── closing_loc: (61...66) = "HERE\n"
diff --git a/test/yarp/snapshots/whitequark/interp_digit_var.txt b/test/yarp/snapshots/whitequark/interp_digit_var.txt
index 4a29381c..91c3664b 100644
--- a/test/yarp/snapshots/whitequark/interp_digit_var.txt
+++ b/test/yarp/snapshots/whitequark/interp_digit_var.txt
@@ -199,33 +199,57 @@
         │   ├── content_loc: (346...350) = "\#@@1"
         │   ├── closing_loc: (350...351) = "`"
         │   └── unescaped: "\#@@1"
-        ├── @ StringNode (location: (354...363))
+        ├── @ InterpolatedStringNode (location: (354...363))
         │   ├── opening_loc: (354...363) = "<<-\"HERE\""
-        │   ├── content_loc: (364...368) = "\#@1\n"
-        │   ├── closing_loc: (368...373) = "HERE\n"
-        │   └── unescaped: "\#@1\n"
-        ├── @ StringNode (location: (374...383))
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (364...368))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (364...368) = "\#@1\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "\#@1\n"
+        │   └── closing_loc: (368...373) = "HERE\n"
+        ├── @ InterpolatedStringNode (location: (374...383))
         │   ├── opening_loc: (374...383) = "<<-\"HERE\""
-        │   ├── content_loc: (384...389) = "\#@@1\n"
-        │   ├── closing_loc: (389...394) = "HERE\n"
-        │   └── unescaped: "\#@@1\n"
-        ├── @ StringNode (location: (395...404))
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (384...389))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (384...389) = "\#@@1\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "\#@@1\n"
+        │   └── closing_loc: (389...394) = "HERE\n"
+        ├── @ InterpolatedStringNode (location: (395...404))
         │   ├── opening_loc: (395...404) = "<<-'HERE'"
-        │   ├── content_loc: (405...409) = "\#@1\n"
-        │   ├── closing_loc: (409...414) = "HERE\n"
-        │   └── unescaped: "\#@1\n"
-        ├── @ StringNode (location: (415...424))
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (405...409))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (405...409) = "\#@1\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "\#@1\n"
+        │   └── closing_loc: (409...414) = "HERE\n"
+        ├── @ InterpolatedStringNode (location: (415...424))
         │   ├── opening_loc: (415...424) = "<<-'HERE'"
-        │   ├── content_loc: (425...430) = "\#@@1\n"
-        │   ├── closing_loc: (430...435) = "HERE\n"
-        │   └── unescaped: "\#@@1\n"
-        ├── @ XStringNode (location: (436...445))
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (425...430))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (425...430) = "\#@@1\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "\#@@1\n"
+        │   └── closing_loc: (430...435) = "HERE\n"
+        ├── @ InterpolatedXStringNode (location: (436...445))
         │   ├── opening_loc: (436...445) = "<<-`HERE`"
-        │   ├── content_loc: (446...450) = "\#@1\n"
-        │   ├── closing_loc: (450...455) = "HERE\n"
-        │   └── unescaped: "\#@1\n"
-        └── @ XStringNode (location: (456...465))
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (446...450))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (446...450) = "\#@1\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "\#@1\n"
+        │   └── closing_loc: (450...455) = "HERE\n"
+        └── @ InterpolatedXStringNode (location: (456...465))
             ├── opening_loc: (456...465) = "<<-`HERE`"
-            ├── content_loc: (466...471) = "\#@@1\n"
-            ├── closing_loc: (471...476) = "HERE\n"
-            └── unescaped: "\#@@1\n"
+            ├── parts: (length: 1)
+            │   └── @ StringNode (location: (466...471))
+            │       ├── opening_loc: ∅
+            │       ├── content_loc: (466...471) = "\#@@1\n"
+            │       ├── closing_loc: ∅
+            │       └── unescaped: "\#@@1\n"
+            └── closing_loc: (471...476) = "HERE\n"
diff --git a/test/yarp/snapshots/whitequark/parser_bug_640.txt b/test/yarp/snapshots/whitequark/parser_bug_640.txt
index 907ada3c..773d2880 100644
--- a/test/yarp/snapshots/whitequark/parser_bug_640.txt
+++ b/test/yarp/snapshots/whitequark/parser_bug_640.txt
@@ -3,8 +3,12 @@
 └── statements:
     @ StatementsNode (location: (0...6))
     └── body: (length: 1)
-        └── @ StringNode (location: (0...6))
+        └── @ InterpolatedStringNode (location: (0...6))
             ├── opening_loc: (0...6) = "<<~FOO"
-            ├── content_loc: (7...20) = "  baz\\\n  qux\n"
-            ├── closing_loc: (20...24) = "FOO\n"
-            └── unescaped: "bazqux\n"
+            ├── parts: (length: 1)
+            │   └── @ StringNode (location: (7...20))
+            │       ├── opening_loc: ∅
+            │       ├── content_loc: (7...20) = "  baz\\\n  qux\n"
+            │       ├── closing_loc: ∅
+            │       └── unescaped: "bazqux\n"
+            └── closing_loc: (20...24) = "FOO\n"
diff --git a/test/yarp/snapshots/whitequark/parser_slash_slash_n_escaping_in_literals.txt b/test/yarp/snapshots/whitequark/parser_slash_slash_n_escaping_in_literals.txt
index 22f43ce7..c76b8f98 100644
--- a/test/yarp/snapshots/whitequark/parser_slash_slash_n_escaping_in_literals.txt
+++ b/test/yarp/snapshots/whitequark/parser_slash_slash_n_escaping_in_literals.txt
@@ -96,21 +96,33 @@
         │   ├── value_loc: (134...138) = "a\\\nb"
         │   ├── closing_loc: (138...139) = "'"
         │   └── unescaped: "ab"
-        ├── @ StringNode (location: (141...150))
+        ├── @ InterpolatedStringNode (location: (141...150))
         │   ├── opening_loc: (141...150) = "<<-\"HERE\""
-        │   ├── content_loc: (151...156) = "a\\\nb\n"
-        │   ├── closing_loc: (156...161) = "HERE\n"
-        │   └── unescaped: "ab\n"
-        ├── @ StringNode (location: (162...171))
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (151...156))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (151...156) = "a\\\nb\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "ab\n"
+        │   └── closing_loc: (156...161) = "HERE\n"
+        ├── @ InterpolatedStringNode (location: (162...171))
         │   ├── opening_loc: (162...171) = "<<-'HERE'"
-        │   ├── content_loc: (172...177) = "a\\\nb\n"
-        │   ├── closing_loc: (177...182) = "HERE\n"
-        │   └── unescaped: "a\\\nb\n"
-        ├── @ XStringNode (location: (183...192))
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (172...177))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (172...177) = "a\\\nb\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "a\\\nb\n"
+        │   └── closing_loc: (177...182) = "HERE\n"
+        ├── @ InterpolatedXStringNode (location: (183...192))
         │   ├── opening_loc: (183...192) = "<<-`HERE`"
-        │   ├── content_loc: (193...198) = "a\\\nb\n"
-        │   ├── closing_loc: (198...203) = "HERE\n"
-        │   └── unescaped: "ab\n"
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (193...198))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (193...198) = "a\\\nb\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "ab\n"
+        │   └── closing_loc: (198...203) = "HERE\n"
         └── @ XStringNode (location: (204...210))
             ├── opening_loc: (204...205) = "`"
             ├── content_loc: (205...209) = "a\\\nb"
diff --git a/test/yarp/snapshots/whitequark/ruby_bug_11989.txt b/test/yarp/snapshots/whitequark/ruby_bug_11989.txt
index a15435f2..4ef84667 100644
--- a/test/yarp/snapshots/whitequark/ruby_bug_11989.txt
+++ b/test/yarp/snapshots/whitequark/ruby_bug_11989.txt
@@ -11,11 +11,15 @@
             ├── arguments:
             │   @ ArgumentsNode (location: (2...8))
             │   └── arguments: (length: 1)
-            │       └── @ StringNode (location: (2...8))
+            │       └── @ InterpolatedStringNode (location: (2...8))
             │           ├── opening_loc: (2...8) = "<<~\"E\""
-            │           ├── content_loc: (9...19) = "  x\\n   y\n"
-            │           ├── closing_loc: (19...21) = "E\n"
-            │           └── unescaped: "x\n   y\n"
+            │           ├── parts: (length: 1)
+            │           │   └── @ StringNode (location: (9...19))
+            │           │       ├── opening_loc: ∅
+            │           │       ├── content_loc: (9...19) = "  x\\n   y\n"
+            │           │       ├── closing_loc: ∅
+            │           │       └── unescaped: "x\n   y\n"
+            │           └── closing_loc: (19...21) = "E\n"
             ├── closing_loc: ∅
             ├── block: ∅
             ├── flags: 
diff --git a/test/yarp/snapshots/whitequark/ruby_bug_11990.txt b/test/yarp/snapshots/whitequark/ruby_bug_11990.txt
index 5bdb43c0..13e89f8c 100644
--- a/test/yarp/snapshots/whitequark/ruby_bug_11990.txt
+++ b/test/yarp/snapshots/whitequark/ruby_bug_11990.txt
@@ -13,11 +13,15 @@
             │   └── arguments: (length: 1)
             │       └── @ StringConcatNode (location: (2...12))
             │           ├── left:
-            │           │   @ StringNode (location: (2...6))
+            │           │   @ InterpolatedStringNode (location: (2...6))
             │           │   ├── opening_loc: (2...6) = "<<~E"
-            │           │   ├── content_loc: (13...17) = "  x\n"
-            │           │   ├── closing_loc: (17...19) = "E\n"
-            │           │   └── unescaped: "x\n"
+            │           │   ├── parts: (length: 1)
+            │           │   │   └── @ StringNode (location: (13...17))
+            │           │   │       ├── opening_loc: ∅
+            │           │   │       ├── content_loc: (13...17) = "  x\n"
+            │           │   │       ├── closing_loc: ∅
+            │           │   │       └── unescaped: "x\n"
+            │           │   └── closing_loc: (17...19) = "E\n"
             │           └── right:
             │               @ StringNode (location: (7...12))
             │               ├── opening_loc: (7...8) = "\""
diff --git a/test/yarp/snapshots/whitequark/slash_newline_in_heredocs.txt b/test/yarp/snapshots/whitequark/slash_newline_in_heredocs.txt
index 124672b5..150325e6 100644
--- a/test/yarp/snapshots/whitequark/slash_newline_in_heredocs.txt
+++ b/test/yarp/snapshots/whitequark/slash_newline_in_heredocs.txt
@@ -3,13 +3,21 @@
 └── statements:
     @ StatementsNode (location: (0...33))
     └── body: (length: 2)
-        ├── @ StringNode (location: (0...4))
+        ├── @ InterpolatedStringNode (location: (0...4))
         │   ├── opening_loc: (0...4) = "<<-E"
-        │   ├── content_loc: (5...25) = "    1 \\\n    2\n    3\n"
-        │   ├── closing_loc: (25...27) = "E\n"
-        │   └── unescaped: "    1     2\n    3\n"
-        └── @ StringNode (location: (29...33))
+        │   ├── parts: (length: 1)
+        │   │   └── @ StringNode (location: (5...25))
+        │   │       ├── opening_loc: ∅
+        │   │       ├── content_loc: (5...25) = "    1 \\\n    2\n    3\n"
+        │   │       ├── closing_loc: ∅
+        │   │       └── unescaped: "    1     2\n    3\n"
+        │   └── closing_loc: (25...27) = "E\n"
+        └── @ InterpolatedStringNode (location: (29...33))
             ├── opening_loc: (29...33) = "<<~E"
-            ├── content_loc: (34...54) = "    1 \\\n    2\n    3\n"
-            ├── closing_loc: (54...56) = "E\n"
-            └── unescaped: "1 2\n3\n"
+            ├── parts: (length: 1)
+            │   └── @ StringNode (location: (34...54))
+            │       ├── opening_loc: ∅
+            │       ├── content_loc: (34...54) = "    1 \\\n    2\n    3\n"
+            │       ├── closing_loc: ∅
+            │       └── unescaped: "1 2\n3\n"
+            └── closing_loc: (54...56) = "E\n"