Skip to main content

big_code_analysis_ast/
alterator.rs

1// Per-language metric and AST modules deliberately consume the macro-
2// generated tree-sitter token enums via `use crate::*` and `use Foo::*`
3// inside match expressions — explicit imports would list dozens of
4// variants per arm and obscure the per-language token sets that are the
5// point of these files. Allowed at the module level rather than per
6// function so the per-language impl blocks stay readable.
7#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
8
9//! Per-language AST simplification for the dump: the [`Alterator`] hook a
10//! language implements to reshape a node before it is rendered as an
11//! [`AstNode`].
12
13use crate::lang_helpers::irules::BRACED_WORD_KINDS as IRULES_BRACED_WORD_KINDS;
14use crate::lang_helpers::tcl::BRACED_WORD_KINDS as TCL_BRACED_WORD_KINDS;
15use crate::*;
16
17/// A trait to create a richer `AST` node for a programming language, mainly
18/// thought to be sent on the network. Crate-internal extension over
19/// [`Checker`], used only by the per-language `Parser<T>` impls.
20pub trait Alterator
21where
22    Self: Checker,
23{
24    /// Creates a new `AST` node containing the code associated to the node,
25    /// its span, the grammar field name through which the parent reaches
26    /// it (if any), and its children.
27    ///
28    /// This function can be overloaded according to the needs of each
29    /// programming language.
30    #[must_use]
31    fn alterate(
32        node: &Node,
33        code: &[u8],
34        span: bool,
35        field_name: Option<&'static str>,
36        children: Vec<AstNode>,
37    ) -> AstNode {
38        Self::get_default(node, code, span, field_name, children)
39    }
40
41    /// Gets the code as text and the span associated to a node.
42    #[must_use]
43    fn get_text_span(node: &Node, code: &[u8], span: bool, text: bool) -> (String, Option<Span>) {
44        let text = if text {
45            // Source may contain non-UTF-8 byte strings (e.g. binary literals); replacement
46            // characters are acceptable in the AST payload produced by dump functions.
47            String::from_utf8_lossy(&code[node.start_byte()..node.end_byte()]).into_owned()
48        } else {
49            String::new()
50        };
51        if span {
52            let (spos_row, spos_column) = node.start_position();
53            let (epos_row, epos_column) = node.end_position();
54            // Tree-sitter positions are 0-based; the dump shape reports
55            // 1-based rows and columns. Byte offsets stay 0-based and
56            // half-open, mirroring tree-sitter (#727).
57            (
58                text,
59                Some(Span::new(
60                    spos_row + 1,
61                    spos_column + 1,
62                    epos_row + 1,
63                    epos_column + 1,
64                    node.start_byte(),
65                    node.end_byte(),
66                )),
67            )
68        } else {
69            (text, None)
70        }
71    }
72
73    /// Gets a default `AST` node containing the code associated to the
74    /// node, its span, its grammar field name (if any), and its
75    /// children.
76    #[must_use]
77    fn get_default(
78        node: &Node,
79        code: &[u8],
80        span: bool,
81        field_name: Option<&'static str>,
82        children: Vec<AstNode>,
83    ) -> AstNode {
84        let (text, span) = Self::get_text_span(node, code, span, node.child_count() == 0);
85        AstNode::with_field_name(node.kind(), text, span, field_name, children)
86    }
87
88    /// Whether `node` must keep its children even though
89    /// [`Self::alterate`] would flatten it into a verbatim leaf.
90    ///
91    /// The flattening arms are keyed on node *kind*, which is the right
92    /// question for a language whose string literals have a kind of
93    /// their own. The Tcl family is the exception: `braced_word` is both
94    /// the literal of `lappend x {a b}` and the *script* of a `proc`
95    /// body or an iRules `when` handler, so flattening by kind dropped
96    /// every script body from the dump — the whole body collapsed into
97    /// one leaf holding its text (#1381).
98    ///
99    /// It is a veto rather than an extra `alterate` arm so that
100    /// `alterate` keeps its byte-and-kind signature: only this question
101    /// needs the ancestor chain, and threading it through all twenty-odd
102    /// `alterate` impls to serve two of them would make every language
103    /// pay for one grammar's ambiguity. `ancestors` is the chain the
104    /// dump walk descended through, for [`Node::parent`]'s `O(depth)`
105    /// reason (#1084) — off an unknown chain this question took
106    /// `Ast::dump` on 8 KB of nested Tcl braces from 10 ms to 418 ms —
107    /// the run `ast.rs` records beside the chain this reads, not a
108    /// second measurement.
109    #[inline]
110    #[must_use]
111    fn keeps_children<'a>(_node: &Node<'a>, _code: &[u8], _ancestors: Ancestors<'a, '_>) -> bool {
112        false
113    }
114
115    /// Gets a new `AST` node if and only if the code is not a comment,
116    /// otherwise [`None`] is returned.
117    ///
118    /// Parameter order mirrors [`Self::alterate`] and [`Self::get_default`]
119    /// (the flags-before-data convention `span, comment, field_name,
120    /// children`) so positional confusion between adjacent boolean
121    /// toggles is harder to introduce on the next edit. `ancestors` is
122    /// last, matching every other chain-taking predicate in the crate.
123    #[must_use]
124    fn get_ast_node<'a>(
125        node: &Node<'a>,
126        code: &[u8],
127        span: bool,
128        comment: bool,
129        field_name: Option<&'static str>,
130        children: Vec<AstNode>,
131        ancestors: Ancestors<'a, '_>,
132    ) -> Option<AstNode> {
133        if comment && Self::is_comment(node) {
134            None
135        } else if Self::keeps_children(node, code, ancestors) {
136            Some(Self::get_default(node, code, span, field_name, children))
137        } else {
138            Some(Self::alterate(node, code, span, field_name, children))
139        }
140    }
141}
142
143impl Alterator for PreprocCode {}
144
145impl Alterator for CcommentCode {}
146
147impl Alterator for CppCode {
148    fn alterate(
149        node: &Node,
150        code: &[u8],
151        span: bool,
152        field_name: Option<&'static str>,
153        mut children: Vec<AstNode>,
154    ) -> AstNode {
155        match Cpp::from(node.kind_id()) {
156            // RawStringLiteral (`R"(…)"`) is flattened alongside
157            // StringLiteral/CharLiteral so the AST dump matches what
158            // `Checker::is_string` already treats as a single
159            // string-like token. Without this arm, raw strings fall
160            // through to `get_default` and render with their
161            // structured delimiter / `string_content` children
162            // — see issue #398 (peer of #391 for Rust).
163            // ConcatenatedString (`"a" "b"`) is one string-like literal
164            // that `Checker::is_string` matches; flatten it too so the
165            // dump collapses its adjacent `string_literal` children
166            // rather than diverging from `is_string` (#699).
167            // CharLiteral is operand + flattened but deliberately absent
168            // from `Checker::is_string` (a char is not a string) — the
169            // same split Rust/Go apply to their char / rune literals.
170            Cpp::StringLiteral
171            | Cpp::CharLiteral
172            | Cpp::RawStringLiteral
173            | Cpp::ConcatenatedString => {
174                let (text, span) = Self::get_text_span(node, code, span, true);
175                AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
176            }
177            Cpp::PreprocDef | Cpp::PreprocFunctionDef | Cpp::PreprocCall => {
178                if let Some(last) = children.last()
179                    && last.r#type == "\n"
180                {
181                    children.pop();
182                }
183                Self::get_default(node, code, span, field_name, children)
184            }
185            _ => Self::get_default(node, code, span, field_name, children),
186        }
187    }
188}
189
190impl Alterator for CCode {
191    fn alterate(
192        node: &Node,
193        code: &[u8],
194        span: bool,
195        field_name: Option<&'static str>,
196        mut children: Vec<AstNode>,
197    ) -> AstNode {
198        match C::from(node.kind_id()) {
199            // RawStringLiteral (`R"(…)"`) is flattened alongside
200            // StringLiteral/CharLiteral so the AST dump matches what
201            // `Checker::is_string` already treats as a single
202            // string-like token. Without this arm, raw strings fall
203            // through to `get_default` and render with their
204            // structured delimiter / `string_content` children
205            // — see issue #398 (peer of #391 for Rust).
206            // ConcatenatedString (`"a" "b"`) is one string-like literal
207            // that `Checker::is_string` matches; flatten it too so the
208            // dump collapses its adjacent `string_literal` children
209            // rather than diverging from `is_string` (#699).
210            // CharLiteral is operand + flattened but deliberately absent
211            // from `Checker::is_string` (a char is not a string) — the
212            // same split Rust/Go apply to their char / rune literals.
213            // C has no raw string literals.
214            C::StringLiteral | C::CharLiteral | C::ConcatenatedString => {
215                let (text, span) = Self::get_text_span(node, code, span, true);
216                AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
217            }
218            C::PreprocDef | C::PreprocFunctionDef | C::PreprocCall => {
219                if let Some(last) = children.last()
220                    && last.r#type == "\n"
221                {
222                    children.pop();
223                }
224                Self::get_default(node, code, span, field_name, children)
225            }
226            _ => Self::get_default(node, code, span, field_name, children),
227        }
228    }
229}
230
231impl Alterator for ObjcCode {
232    fn alterate(
233        node: &Node,
234        code: &[u8],
235        span: bool,
236        field_name: Option<&'static str>,
237        mut children: Vec<AstNode>,
238    ) -> AstNode {
239        // ObjC is C plus message sends; the literal-flattening and
240        // preprocessor newline-trimming rules are identical to the C
241        // alterator. `@"…"` is one `string_literal` whose first child
242        // is the `@` token, so the `StringLiteral` arm flattens the
243        // whole literal, marker included, exactly as for C.
244        match Objc::from(node.kind_id()) {
245            // CharLiteral is operand + flattened but deliberately absent
246            // from `Checker::is_string` (a char is not a string) — the
247            // same split Rust/Go apply to their char / rune literals.
248            // The C / Cpp / Mozcpp sibling arms have carried this note
249            // since #699; the ObjC clone was missing it (#1316), which
250            // is how the missing operand half stayed unnoticed here
251            // longest.
252            Objc::StringLiteral | Objc::CharLiteral | Objc::ConcatenatedString => {
253                let (text, span) = Self::get_text_span(node, code, span, true);
254                AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
255            }
256            Objc::PreprocDef | Objc::PreprocFunctionDef | Objc::PreprocCall => {
257                if let Some(last) = children.last()
258                    && last.r#type == "\n"
259                {
260                    children.pop();
261                }
262                Self::get_default(node, code, span, field_name, children)
263            }
264            _ => Self::get_default(node, code, span, field_name, children),
265        }
266    }
267}
268
269impl Alterator for MozcppCode {
270    fn alterate(
271        node: &Node,
272        code: &[u8],
273        span: bool,
274        field_name: Option<&'static str>,
275        mut children: Vec<AstNode>,
276    ) -> AstNode {
277        match Mozcpp::from(node.kind_id()) {
278            // RawStringLiteral (`R"(…)"`) is flattened alongside
279            // StringLiteral/CharLiteral so the AST dump matches what
280            // `Checker::is_string` already treats as a single
281            // string-like token. Without this arm, raw strings fall
282            // through to `get_default` and render with their
283            // structured delimiter / `string_content` children
284            // — see issue #398 (peer of #391 for Rust).
285            // ConcatenatedString (`"a" "b"`) is one string-like literal
286            // that `Checker::is_string` matches; flatten it too so the
287            // dump collapses its adjacent `string_literal` children
288            // rather than diverging from `is_string` (#699).
289            // CharLiteral is operand + flattened but deliberately absent
290            // from `Checker::is_string` (a char is not a string) — the
291            // same split Rust/Go apply to their char / rune literals.
292            Mozcpp::StringLiteral
293            | Mozcpp::CharLiteral
294            | Mozcpp::RawStringLiteral
295            | Mozcpp::ConcatenatedString => {
296                let (text, span) = Self::get_text_span(node, code, span, true);
297                AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
298            }
299            Mozcpp::PreprocDef | Mozcpp::PreprocFunctionDef | Mozcpp::PreprocCall => {
300                if let Some(last) = children.last()
301                    && last.r#type == "\n"
302                {
303                    children.pop();
304                }
305                Self::get_default(node, code, span, field_name, children)
306            }
307            _ => Self::get_default(node, code, span, field_name, children),
308        }
309    }
310}
311
312impl Alterator for PythonCode {
313    fn alterate(
314        node: &Node,
315        code: &[u8],
316        span: bool,
317        field_name: Option<&'static str>,
318        children: Vec<AstNode>,
319    ) -> AstNode {
320        match Python::from(node.kind_id()) {
321            // `String` (covers f-strings, whose `{…}` interpolations are
322            // collapsed into the flat text payload — same convention as
323            // PHP `EncapsedString` / Ruby interpolated strings / C#
324            // `InterpolatedStringExpression`) and `ConcatenatedString`
325            // (`"a" "b"`) are the kinds `Checker::is_string` matches;
326            // flatten both so the dump agrees with `is_string` (#699).
327            Python::String | Python::ConcatenatedString => {
328                let (text, span) = Self::get_text_span(node, code, span, true);
329                AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
330            }
331            _ => Self::get_default(node, code, span, field_name, children),
332        }
333    }
334}
335
336impl Alterator for JavaCode {
337    fn alterate(
338        node: &Node,
339        code: &[u8],
340        span: bool,
341        field_name: Option<&'static str>,
342        children: Vec<AstNode>,
343    ) -> AstNode {
344        match Java::from(node.kind_id()) {
345            // `StringLiteral` and `MultilineStringLiteral` (text blocks,
346            // `"""…"""`) are the kinds `Checker::is_string` matches;
347            // flatten both so the dump collapses their `string_fragment`
348            // children rather than diverging from `is_string` (#699).
349            Java::StringLiteral | Java::MultilineStringLiteral => {
350                let (text, span) = Self::get_text_span(node, code, span, true);
351                AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
352            }
353            _ => Self::get_default(node, code, span, field_name, children),
354        }
355    }
356}
357
358impl Alterator for KotlinCode {
359    fn alterate(
360        node: &Node,
361        code: &[u8],
362        span: bool,
363        field_name: Option<&'static str>,
364        children: Vec<AstNode>,
365    ) -> AstNode {
366        match Kotlin::from(node.kind_id()) {
367            // `StringLiteral` (whose `${…}` interpolations are collapsed
368            // into the flat text payload) and `MultilineStringLiteral`
369            // (`"""…"""`) are the kinds `Checker::is_string` matches;
370            // flatten both so the dump agrees with `is_string` (#699).
371            Kotlin::StringLiteral | Kotlin::MultilineStringLiteral => {
372                let (text, span) = Self::get_text_span(node, code, span, true);
373                AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
374            }
375            _ => Self::get_default(node, code, span, field_name, children),
376        }
377    }
378}
379
380impl Alterator for CsharpCode {
381    fn alterate(
382        node: &Node,
383        code: &[u8],
384        span: bool,
385        field_name: Option<&'static str>,
386        children: Vec<AstNode>,
387    ) -> AstNode {
388        match Csharp::from(node.kind_id()) {
389            Csharp::StringLiteral
390            | Csharp::VerbatimStringLiteral
391            | Csharp::RawStringLiteral
392            | Csharp::InterpolatedStringExpression
393            | Csharp::CharacterLiteral => {
394                let (text, span) = Self::get_text_span(node, code, span, true);
395                AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
396            }
397            _ => Self::get_default(node, code, span, field_name, children),
398        }
399    }
400}
401
402impl Alterator for GoCode {
403    fn alterate(
404        node: &Node,
405        code: &[u8],
406        span: bool,
407        field_name: Option<&'static str>,
408        children: Vec<AstNode>,
409    ) -> AstNode {
410        match Go::from(node.kind_id()) {
411            Go::InterpretedStringLiteral | Go::RawStringLiteral | Go::RuneLiteral => {
412                let (text, span) = Self::get_text_span(node, code, span, true);
413                AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
414            }
415            _ => Self::get_default(node, code, span, field_name, children),
416        }
417    }
418}
419
420impl Alterator for LuaCode {
421    fn alterate(
422        node: &Node,
423        code: &[u8],
424        span: bool,
425        field_name: Option<&'static str>,
426        children: Vec<AstNode>,
427    ) -> AstNode {
428        match Lua::from(node.kind_id()) {
429            Lua::String => {
430                let (text, span) = Self::get_text_span(node, code, span, true);
431                AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
432            }
433            _ => Self::get_default(node, code, span, field_name, children),
434        }
435    }
436}
437
438impl Alterator for MozjsCode {
439    fn alterate(
440        node: &Node,
441        code: &[u8],
442        span: bool,
443        field_name: Option<&'static str>,
444        children: Vec<AstNode>,
445    ) -> AstNode {
446        match Mozjs::from(node.kind_id()) {
447            // `String`/`String2` (the anonymous keyword alias) and
448            // `TemplateString` are the kinds `Checker::is_string`
449            // matches. `TemplateString` may carry `${…}` interpolation
450            // children; collapsing them into the flat text payload here
451            // is intentional, matching the dump convention for other
452            // interpolating string literals (PHP `EncapsedString`, Ruby,
453            // C# `InterpolatedStringExpression`). Flatten all three so the
454            // dump agrees with `is_string` (#699; before this the comment
455            // claimed `TemplateString` flattening the arm did not do).
456            Mozjs::String | Mozjs::String2 | Mozjs::TemplateString => {
457                let (text, span) = Self::get_text_span(node, code, span, true);
458                AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
459            }
460            _ => Self::get_default(node, code, span, field_name, children),
461        }
462    }
463}
464
465impl Alterator for JavascriptCode {
466    fn alterate(
467        node: &Node,
468        code: &[u8],
469        span: bool,
470        field_name: Option<&'static str>,
471        children: Vec<AstNode>,
472    ) -> AstNode {
473        match Javascript::from(node.kind_id()) {
474            // `TemplateString` joins `String`/`String2` so the dump
475            // matches `Checker::is_string`; its `${…}` interpolations
476            // collapse into the flat text payload (#699).
477            Javascript::String | Javascript::String2 | Javascript::TemplateString => {
478                let (text, span) = Self::get_text_span(node, code, span, true);
479                AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
480            }
481            _ => Self::get_default(node, code, span, field_name, children),
482        }
483    }
484}
485
486impl Alterator for TypescriptCode {
487    fn alterate(
488        node: &Node,
489        code: &[u8],
490        span: bool,
491        field_name: Option<&'static str>,
492        children: Vec<AstNode>,
493    ) -> AstNode {
494        match Typescript::from(node.kind_id()) {
495            // `TemplateString` joins `String` so the dump matches
496            // `Checker::is_string`; its `${…}` interpolations collapse
497            // into the flat text payload (#699). TS's `String2` (the
498            // `: string` type keyword, a childless leaf for which this
499            // arm is a no-op anyway) left the string set with #1261.
500            Typescript::String | Typescript::TemplateString => {
501                let (text, span) = Self::get_text_span(node, code, span, true);
502                AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
503            }
504            _ => Self::get_default(node, code, span, field_name, children),
505        }
506    }
507}
508
509impl Alterator for TsxCode {
510    fn alterate(
511        node: &Node,
512        code: &[u8],
513        span: bool,
514        field_name: Option<&'static str>,
515        children: Vec<AstNode>,
516    ) -> AstNode {
517        match Tsx::from(node.kind_id()) {
518            // `TemplateString` joins `String`/`String2` (the
519            // string-literal alias) so the dump matches
520            // `Checker::is_string`; its `${…}` interpolations collapse
521            // into the flat text payload (#699). `String3` (the
522            // `: string` type keyword, a childless leaf for which this
523            // arm is a no-op anyway) left the string set with #1261.
524            Tsx::String | Tsx::String2 | Tsx::TemplateString => {
525                let (text, span) = Self::get_text_span(node, code, span, true);
526                AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
527            }
528            _ => Self::get_default(node, code, span, field_name, children),
529        }
530    }
531}
532
533impl Alterator for RustCode {
534    fn alterate(
535        node: &Node,
536        code: &[u8],
537        span: bool,
538        field_name: Option<&'static str>,
539        children: Vec<AstNode>,
540    ) -> AstNode {
541        match Rust::from(node.kind_id()) {
542            // RawStringLiteral (`r#"…"#`) is flattened alongside
543            // StringLiteral/CharLiteral so the AST dump matches what
544            // `Checker::is_string` and `Getter::get_op_type` already
545            // treat as a single string-like token. Without this arm,
546            // raw strings fall through to `get_default` and render
547            // with their structured `_raw_string_literal_start` /
548            // `string_content` / `_raw_string_literal_end` children
549            // — see issue #391.
550            Rust::StringLiteral | Rust::RawStringLiteral | Rust::CharLiteral => {
551                let (text, span) = Self::get_text_span(node, code, span, true);
552                AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
553            }
554            _ => Self::get_default(node, code, span, field_name, children),
555        }
556    }
557}
558
559impl Alterator for PerlCode {
560    fn alterate(
561        node: &Node,
562        code: &[u8],
563        span: bool,
564        field_name: Option<&'static str>,
565        children: Vec<AstNode>,
566    ) -> AstNode {
567        match Perl::from(node.kind_id()) {
568            // `HeredocBodyStatement` (`<<TAG … TAG`) is the kind
569            // `Checker::is_string` matches for heredoc bodies; flatten it
570            // so its `Interpolation` children collapse into the flat text
571            // payload rather than diverging from `is_string` (#761, the
572            // gap #699 missed — same convention as Ruby `heredoc_body` /
573            // PHP `heredoc`/`nowdoc`).
574            Perl::StringSingleQuoted
575            | Perl::StringDoubleQuoted
576            | Perl::StringQQuoted
577            | Perl::StringQqQuoted
578            | Perl::BacktickQuoted
579            | Perl::CommandQxQuoted
580            | Perl::HeredocBodyStatement => {
581                let (text, span) = Self::get_text_span(node, code, span, true);
582                AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
583            }
584            _ => Self::get_default(node, code, span, field_name, children),
585        }
586    }
587}
588
589impl Alterator for BashCode {
590    fn alterate(
591        node: &Node,
592        code: &[u8],
593        span: bool,
594        field_name: Option<&'static str>,
595        children: Vec<AstNode>,
596    ) -> AstNode {
597        match Bash::from(node.kind_id()) {
598            // `HeredocBody2` (`<<EOF … EOF`, kind `heredoc_body`) is the
599            // kind `Checker::is_string` matches for heredoc bodies; flatten
600            // it so its interpolation / fragment children collapse into the
601            // flat text payload rather than diverging from `is_string`
602            // (#761, the gap #699 missed — same convention as Ruby
603            // `heredoc_body` / PHP `heredoc`/`nowdoc`).
604            Bash::String
605            | Bash::RawString
606            | Bash::AnsiCString
607            | Bash::TranslatedString
608            | Bash::HeredocBody2 => {
609                let (text, span) = Self::get_text_span(node, code, span, true);
610                AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
611            }
612            _ => Self::get_default(node, code, span, field_name, children),
613        }
614    }
615}
616
617impl Alterator for TclCode {
618    fn alterate(
619        node: &Node,
620        code: &[u8],
621        span: bool,
622        field_name: Option<&'static str>,
623        children: Vec<AstNode>,
624    ) -> AstNode {
625        match Tcl::from(node.kind_id()) {
626            // Preserve string literals verbatim to avoid whitespace trimming.
627            // `BracedWord` is listed for the braced *value* of
628            // `lappend x {a b}` — Tcl evaluates nothing between its
629            // braces, so the value is its whole span. The same kind is
630            // also every script body, which must keep its children;
631            // `keeps_children` below vetoes this arm for those, because
632            // only the enclosing command separates the two roles and
633            // that question needs the ancestor chain (#1381).
634            Tcl::QuotedWord | Tcl::BracedWord | Tcl::BracedWordSimple => {
635                let (text, span) = Self::get_text_span(node, code, span, true);
636                AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
637            }
638            _ => Self::get_default(node, code, span, field_name, children),
639        }
640    }
641
642    fn keeps_children<'a>(node: &Node<'a>, code: &[u8], ancestors: Ancestors<'a, '_>) -> bool {
643        <Self as Getter>::is_braced_script_word(node, code, ancestors, &TCL_BRACED_WORD_KINDS)
644    }
645}
646
647impl Alterator for IrulesCode {
648    fn alterate(
649        node: &Node,
650        code: &[u8],
651        span: bool,
652        field_name: Option<&'static str>,
653        children: Vec<AstNode>,
654    ) -> AstNode {
655        match Irules::from(node.kind_id()) {
656            // Preserve string literals verbatim to avoid whitespace trimming.
657            // The twin of the Tcl arm above, `keeps_children` veto and
658            // all. It matters more here: a `when` handler's body is the
659            // whole of a typical iRules file, so flattening every one of
660            // them left the dump with a single leaf per handler and no
661            // structure at all (#1381).
662            Irules::QuotedWord | Irules::BracedWord | Irules::BracedWordSimple => {
663                let (text, span) = Self::get_text_span(node, code, span, true);
664                AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
665            }
666            _ => Self::get_default(node, code, span, field_name, children),
667        }
668    }
669
670    fn keeps_children<'a>(node: &Node<'a>, code: &[u8], ancestors: Ancestors<'a, '_>) -> bool {
671        <Self as Getter>::is_braced_script_word(node, code, ancestors, &IRULES_BRACED_WORD_KINDS)
672    }
673}
674
675impl Alterator for PhpCode {
676    fn alterate(
677        node: &Node,
678        code: &[u8],
679        span: bool,
680        field_name: Option<&'static str>,
681        children: Vec<AstNode>,
682    ) -> AstNode {
683        match Php::from(node.kind_id()) {
684            // `String`/`String2`/`String3` are all aliased kind_ids
685            // that the enum maps to `"string"`; flatten every alias to
686            // preserve source text (#288, same pattern as #119 for
687            // JS/TS).
688            //
689            // This is a dump-shape contract, not an `is_string` mirror:
690            // #1474 withdrew `String2` (the `string` *type* keyword)
691            // from `Checker::is_string` and #1293 suppressed it in
692            // `Getter::get_op_type`, while it stays listed here. Both
693            // are right — the question this arm answers is "does the
694            // dump keep this node's source text", and the keyword is a
695            // childless leaf, so listing it is a no-op versus
696            // `get_default` (pinned by the test below). Deleting it
697            // would be grammar-dispatch §6's narrow-by-deletion: a
698            // grammar revision that gave the alias children would then
699            // silently start splitting text this arm exists to keep
700            // whole.
701            Php::String
702            | Php::String2
703            | Php::String3
704            | Php::EncapsedString
705            | Php::Heredoc
706            | Php::Nowdoc
707            | Php::ShellCommandExpression => {
708                let (text, span) = Self::get_text_span(node, code, span, true);
709                AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
710            }
711            _ => Self::get_default(node, code, span, field_name, children),
712        }
713    }
714}
715
716impl Alterator for RubyCode {
717    fn alterate(
718        node: &Node,
719        code: &[u8],
720        span: bool,
721        field_name: Option<&'static str>,
722        children: Vec<AstNode>,
723    ) -> AstNode {
724        // Preserve verbatim text for string-like literals (regular strings,
725        // chained strings, heredocs, regexes, subshells, symbol arrays,
726        // delimited/simple symbols, character literals). Interpolation
727        // children are intentionally collapsed into the flat text payload.
728        match Ruby::from(node.kind_id()) {
729            Ruby::String
730            | Ruby::ChainedString
731            | Ruby::BareString
732            | Ruby::Subshell
733            | Ruby::Regex
734            | Ruby::HeredocBody
735            | Ruby::StringArray
736            | Ruby::SymbolArray
737            | Ruby::DelimitedSymbol
738            | Ruby::SimpleSymbol
739            | Ruby::Character => {
740                let (text, span) = Self::get_text_span(node, code, span, true);
741                AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
742            }
743            _ => Self::get_default(node, code, span, field_name, children),
744        }
745    }
746}
747
748impl Alterator for ElixirCode {
749    fn alterate(
750        node: &Node,
751        code: &[u8],
752        span: bool,
753        field_name: Option<&'static str>,
754        children: Vec<AstNode>,
755    ) -> AstNode {
756        // Preserve string-like literal text verbatim. `Charlist` (single-
757        // quoted) and `Sigil` (`~r/.../`, `~w[...]`, etc.) are treated
758        // alongside ordinary strings so report output never trims their
759        // bodies.
760        match Elixir::from(node.kind_id()) {
761            Elixir::String | Elixir::Charlist | Elixir::Sigil => {
762                let (text, span) = Self::get_text_span(node, code, span, true);
763                AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
764            }
765            _ => Self::get_default(node, code, span, field_name, children),
766        }
767    }
768}
769
770impl Alterator for GroovyCode {
771    fn alterate(
772        node: &Node,
773        code: &[u8],
774        span: bool,
775        field_name: Option<&'static str>,
776        children: Vec<AstNode>,
777    ) -> AstNode {
778        // Preserve string and GString fragment text verbatim so report
779        // output never trims their bodies. The dekobon Groovy grammar
780        // ships several `StringFragment*` aliases for the same rule
781        // applied inside different string contexts (single/double-quoted,
782        // triple-quoted, slashy, dollar-slashy); each one carries body
783        // text we must not collapse.
784        match Groovy::from(node.kind_id()) {
785            Groovy::StringLiteral
786            | Groovy::StringFragment
787            | Groovy::StringFragment2
788            | Groovy::StringFragment3
789            | Groovy::StringFragment4
790            | Groovy::StringFragment5 => {
791                let (text, span) = Self::get_text_span(node, code, span, true);
792                AstNode::with_field_name(node.kind(), text, span, field_name, Vec::new())
793            }
794            _ => Self::get_default(node, code, span, field_name, children),
795        }
796    }
797}
798
799#[cfg(test)]
800#[allow(
801    clippy::float_cmp,
802    clippy::cast_precision_loss,
803    clippy::cast_possible_truncation,
804    clippy::cast_sign_loss,
805    clippy::similar_names,
806    clippy::doc_markdown,
807    clippy::needless_raw_string_hashes,
808    clippy::too_many_lines
809)]
810mod tests {
811    use std::path::PathBuf;
812
813    use crate::{CppCode, CppParser, ParserTrait};
814
815    use super::*;
816
817    #[cfg(feature = "cpp")]
818    #[test]
819    fn get_text_span_non_utf8_uses_replacement_char() {
820        // Regression: `String::from_utf8(...).unwrap()` panicked on non-UTF-8
821        // source bytes (e.g. binary literals). Now uses from_utf8_lossy so the
822        // resulting AstNode text contains U+FFFD rather than causing a crash.
823        let code = b"char c = '\xff';";
824        let path = PathBuf::from("test.c");
825        let parser = CppParser::new(code.to_vec(), &path, None);
826        let root = parser.root();
827        let (text, _) = CppCode::get_text_span(&root, code, false, true);
828        assert!(
829            text.contains('\u{FFFD}'),
830            "expected U+FFFD replacement char for non-UTF-8 source, got: {text:?}"
831        );
832    }
833
834    /// Collects all AstNode entries whose type matches `target_kind`,
835    /// recursively walking the tree.
836    #[cfg(any(
837        feature = "bash",
838        feature = "cpp",
839        feature = "go",
840        feature = "groovy",
841        feature = "java",
842        feature = "javascript",
843        feature = "kotlin",
844        feature = "mozjs",
845        feature = "perl",
846        feature = "php",
847        feature = "python",
848        feature = "rust",
849        feature = "typescript",
850    ))]
851    fn collect_nodes_by_kind<'a>(node: &'a AstNode, target_kind: &str, out: &mut Vec<&'a AstNode>) {
852        if node.r#type == target_kind {
853            out.push(node);
854        }
855        for child in &node.children {
856            collect_nodes_by_kind(child, target_kind, out);
857        }
858    }
859
860    /// Builds an AST from source code using the given parser type.
861    #[cfg(any(
862        feature = "bash",
863        feature = "cpp",
864        feature = "go",
865        feature = "groovy",
866        feature = "java",
867        feature = "javascript",
868        feature = "kotlin",
869        feature = "mozjs",
870        feature = "perl",
871        feature = "php",
872        feature = "python",
873        feature = "rust",
874        feature = "typescript",
875    ))]
876    fn build_ast<P: ParserTrait>(code: &[u8], filename: &str) -> AstNode {
877        let path = PathBuf::from(filename);
878        let parser = P::new(code.to_vec(), &path, None);
879        let cfg = crate::AstCfg {
880            id: String::new(),
881            language: String::new(),
882            comment: false,
883            span: false,
884        };
885        let resp = crate::ast::dump_inner(&parser, cfg);
886        resp.root.expect("parser should produce a root AST node")
887    }
888
889    /// Asserts that every `"string"` node in the AST is flattened:
890    /// non-empty text value and no children.
891    #[cfg(any(feature = "javascript", feature = "php", feature = "typescript"))]
892    fn assert_strings_flattened(root: &AstNode) {
893        let mut strings = Vec::new();
894        collect_nodes_by_kind(root, "string", &mut strings);
895        assert!(
896            !strings.is_empty(),
897            "expected at least one 'string' node in the AST"
898        );
899        for node in &strings {
900            assert!(
901                node.children.is_empty(),
902                "string node should be flattened (no children), got {} children; value={:?}",
903                node.children.len(),
904                node.value,
905            );
906            assert!(
907                !node.value.is_empty(),
908                "flattened string node should have non-empty text value"
909            );
910        }
911    }
912
913    // Regression tests for #119: String2 (and String3) variants must be
914    // flattened the same way as String. These exercises string literals in
915    // multiple grammatical positions to cover aliased kind_ids.
916    #[cfg(feature = "javascript")]
917    #[test]
918    fn javascript_string_nodes_all_flattened() {
919        // Strings in expression, property key, and import positions
920        // exercise different grammar productions (String vs String2).
921        let code = br#"
922            const a = 'single';
923            const b = "double";
924            const obj = {"key": 1};
925            import "module";
926        "#;
927        let root = build_ast::<crate::JavascriptParser>(code, "test.js");
928        assert_strings_flattened(&root);
929    }
930
931    #[cfg(feature = "typescript")]
932    #[test]
933    fn typescript_string_nodes_all_flattened() {
934        let code = br#"
935            const a: string = 'single';
936            const b: string = "double";
937            const obj: Record<string, number> = {"key": 1};
938            import "module";
939        "#;
940        let root = build_ast::<crate::TypescriptParser>(code, "test.ts");
941        assert_strings_flattened(&root);
942    }
943
944    #[cfg(feature = "typescript")]
945    #[test]
946    fn tsx_string_nodes_all_flattened() {
947        // TSX has String, String2, and String3 — exercise JSX attribute
948        // strings and regular string expressions.
949        let code = br#"
950            const a = 'single';
951            const b = "double";
952            const el = <div className="cls">{"text"}</div>;
953        "#;
954        let root = build_ast::<crate::TsxParser>(code, "test.tsx");
955        assert_strings_flattened(&root);
956    }
957
958    #[cfg(feature = "php")]
959    #[test]
960    fn php_string_like_nodes_all_flattened() {
961        // Regression: issue #288. PHP `string`, `encapsed_string`,
962        // `heredoc`, `nowdoc`, and `shell_command_expression` (backtick
963        // form) must all flatten through the same `Alterator` arm.
964        // The wave-1 fix also added the `String2`/`String3` enum
965        // aliases to the arm. The current `tree-sitter-php` grammar
966        // emits `String2` only as the `string` type keyword (a
967        // childless terminal, and not a string to `Checker::is_string`
968        // since #1474) and never emits `String3` (a hidden supertype)
969        // as a concrete node, so the arm is a no-op vs `get_default`
970        // for both aliases — but locking the structural contract here
971        // keeps the dump's text preserved if a future grammar revision
972        // surfaces either id as an interior node.
973        let code = br#"<?php
974            $single = 'single';
975            $double = "double";
976            $cmd = `ls`;
977            $here = <<<EOT
978                some text
979                EOT;
980            $now = <<<'EOT'
981                literal
982                EOT;
983        "#;
984        let root = build_ast::<crate::PhpParser>(code, "test.php");
985        assert_strings_flattened(&root);
986        // ShellCommandExpression flattens with the same shape as
987        // EncapsedString — confirm it preserves source text and has
988        // no children.
989        let mut shells = Vec::new();
990        collect_nodes_by_kind(&root, "shell_command_expression", &mut shells);
991        assert!(
992            !shells.is_empty(),
993            "expected at least one shell_command_expression node"
994        );
995        for node in &shells {
996            assert!(
997                node.children.is_empty(),
998                "shell_command_expression should be flattened; got {} children",
999                node.children.len()
1000            );
1001            assert!(
1002                node.value.contains("ls"),
1003                "flattened backtick literal should preserve text; got {:?}",
1004                node.value
1005            );
1006        }
1007    }
1008
1009    #[cfg(feature = "groovy")]
1010    #[test]
1011    fn groovy_string_literal_preserved_verbatim() {
1012        // Regression for the `impl Alterator for GroovyCode` arms:
1013        // the `StringLiteral` arm (and its `StringFragment*` aliases
1014        // for interpolated / slashy / dollar-slashy contexts) must
1015        // keep the literal's text intact.
1016        let code = br#"
1017            class A {
1018                String single = 'hello'
1019                String double = "world"
1020                char ch = 'x'
1021            }
1022        "#;
1023        let root = build_ast::<crate::GroovyParser>(code, "test.groovy");
1024        // The dekobon Groovy grammar consolidates every string shape
1025        // under one `string_literal` kind name (single-quoted,
1026        // double-quoted, triple-quoted, slashy `/.../`, dollar-slashy
1027        // `$/.../$`, GString-interpolated); there is no separate
1028        // `character_literal` kind. The defensive collect-by-name for
1029        // `character_literal` below is a drift marker — it is expected
1030        // to find nothing today but would surface a future grammar
1031        // bump that promoted character literals to a distinct kind.
1032        let mut strings = Vec::new();
1033        collect_nodes_by_kind(&root, "string_literal", &mut strings);
1034        collect_nodes_by_kind(&root, "character_literal", &mut strings);
1035        assert!(
1036            !strings.is_empty(),
1037            "expected at least one string/character literal in the AST"
1038        );
1039        for node in &strings {
1040            assert!(
1041                node.children.is_empty(),
1042                "string-like node should be flattened (no children); got {} children, value={:?}",
1043                node.children.len(),
1044                node.value,
1045            );
1046            assert!(
1047                !node.value.is_empty(),
1048                "flattened string-like node should have non-empty text value"
1049            );
1050        }
1051    }
1052
1053    #[cfg(feature = "groovy")]
1054    #[test]
1055    fn groovy_multiline_string_fragment_preserves_newlines() {
1056        // The `StringLiteral`/`MultilineStringFragment` arms route
1057        // through `get_text_span(..., true)` to keep the body text
1058        // verbatim. A regression that flips that boolean to `false`
1059        // would trim embedded whitespace — including the newlines
1060        // inside triple-quoted strings. The outer `StringLiteral`
1061        // node is flattened by the alterator and carries the entire
1062        // triple-quoted body as its value.
1063        let code = b"def s = \"\"\"first line\nsecond line\nthird line\"\"\"";
1064        let root = build_ast::<crate::GroovyParser>(code, "test.groovy");
1065        let mut strings = Vec::new();
1066        collect_nodes_by_kind(&root, "string_literal", &mut strings);
1067        assert!(
1068            !strings.is_empty(),
1069            "expected at least one string_literal node for the triple-quoted body"
1070        );
1071        let any_with_newline = strings.iter().any(|n| n.value.contains('\n'));
1072        assert!(
1073            any_with_newline,
1074            "expected the flattened string_literal to keep its embedded newlines; got values: {:?}",
1075            strings.iter().map(|n| &n.value).collect::<Vec<_>>()
1076        );
1077    }
1078
1079    #[cfg(feature = "rust")]
1080    #[test]
1081    fn rust_raw_string_literal_flattened() {
1082        // Regression for issue #391: `Rust::RawStringLiteral` was missing
1083        // from the `RustCode` Alterator arm, so `r#"hello"#` rendered with
1084        // structured children (`_raw_string_literal_start`,
1085        // `string_content`, `_raw_string_literal_end`) in the AST dump
1086        // while the regular `"hello"` was flattened to a single text
1087        // node. `Checker::is_string` and `Getter::get_op_type` already
1088        // treat both as equivalent; the alterator now matches.
1089        let code = br##"fn main() { let s = r#"hello"#; let t = "world"; }"##;
1090        let root = build_ast::<crate::RustParser>(code, "test.rs");
1091
1092        let mut raw_strings = Vec::new();
1093        collect_nodes_by_kind(&root, "raw_string_literal", &mut raw_strings);
1094        assert_eq!(
1095            raw_strings.len(),
1096            1,
1097            "expected exactly one raw_string_literal node"
1098        );
1099        let raw = raw_strings[0];
1100        assert_eq!(
1101            raw.children.len(),
1102            0,
1103            "raw_string_literal should be flattened (no children)"
1104        );
1105        assert_eq!(
1106            raw.value, "r#\"hello\"#",
1107            "raw_string_literal should preserve the verbatim source text"
1108        );
1109
1110        // The regular StringLiteral arm must still flatten too — confirm
1111        // the asymmetry that motivated #391 is gone.
1112        let mut strings = Vec::new();
1113        collect_nodes_by_kind(&root, "string_literal", &mut strings);
1114        assert_eq!(strings.len(), 1, "expected exactly one string_literal node");
1115        assert_eq!(
1116            strings[0].children.len(),
1117            0,
1118            "string_literal should be flattened (no children)"
1119        );
1120        assert_eq!(strings[0].value, "\"world\"");
1121    }
1122
1123    #[cfg(feature = "cpp")]
1124    #[test]
1125    fn cpp_raw_string_literal_flattened() {
1126        // Regression for issue #398: `Cpp::RawStringLiteral` was missing
1127        // from the `CppCode` Alterator arm, so `R"(hello)"` rendered with
1128        // structured delimiter / `string_content` children in the AST
1129        // dump while the regular `"hello"` was flattened to a single
1130        // text node. `Checker::is_string` already treats both as
1131        // equivalent; the alterator now matches. Peer of issue #391
1132        // for Rust.
1133        let code = br#"int main() { auto s = R"(hello)"; auto t = "world"; }"#;
1134        let root = build_ast::<crate::CppParser>(code, "test.cpp");
1135
1136        let mut raw_strings = Vec::new();
1137        collect_nodes_by_kind(&root, "raw_string_literal", &mut raw_strings);
1138        assert_eq!(
1139            raw_strings.len(),
1140            1,
1141            "expected exactly one raw_string_literal node"
1142        );
1143        let raw = raw_strings[0];
1144        assert_eq!(
1145            raw.children.len(),
1146            0,
1147            "raw_string_literal should be flattened (no children)"
1148        );
1149        assert_eq!(
1150            raw.value, "R\"(hello)\"",
1151            "raw_string_literal should preserve the verbatim source text"
1152        );
1153
1154        // The regular StringLiteral arm must still flatten too — confirm
1155        // the asymmetry that motivated #398 is gone.
1156        let mut strings = Vec::new();
1157        collect_nodes_by_kind(&root, "string_literal", &mut strings);
1158        assert_eq!(strings.len(), 1, "expected exactly one string_literal node");
1159        assert_eq!(
1160            strings[0].children.len(),
1161            0,
1162            "string_literal should be flattened (no children)"
1163        );
1164        assert_eq!(strings[0].value, "\"world\"");
1165    }
1166
1167    /// Asserts that every node of kind `target_kind` in the AST is
1168    /// flattened (no children) and carries non-empty verbatim text.
1169    /// Shared by the #699 per-language string-flattening regressions.
1170    #[cfg(any(
1171        feature = "bash",
1172        feature = "cpp",
1173        feature = "go",
1174        feature = "java",
1175        feature = "javascript",
1176        feature = "kotlin",
1177        feature = "mozjs",
1178        feature = "perl",
1179        feature = "python",
1180        feature = "typescript",
1181    ))]
1182    fn assert_kind_flattened(root: &AstNode, target_kind: &str) {
1183        let mut nodes = Vec::new();
1184        collect_nodes_by_kind(root, target_kind, &mut nodes);
1185        assert!(
1186            !nodes.is_empty(),
1187            "expected at least one '{target_kind}' node in the AST"
1188        );
1189        for node in &nodes {
1190            assert!(
1191                node.children.is_empty(),
1192                "'{target_kind}' node should be flattened (no children), got {} children; value={:?}",
1193                node.children.len(),
1194                node.value,
1195            );
1196            assert!(
1197                !node.value.is_empty(),
1198                "flattened '{target_kind}' node should have non-empty text value"
1199            );
1200        }
1201    }
1202
1203    // ===== #699: align alterator flattening with `Checker::is_string` =====
1204    // Before #699 these string-like literals kept their structured
1205    // children in the AST dump while `is_string` already treated the whole
1206    // node as a string — a 3-way (alterator / is_string / get_op_type)
1207    // dump asymmetry. Each test pins that the dump now collapses the kind.
1208
1209    #[cfg(feature = "javascript")]
1210    #[test]
1211    fn javascript_template_string_flattened() {
1212        // #699: `is_string` matches `TemplateString`; the alterator now
1213        // flattens it too, collapsing `${…}` interpolation children into
1214        // the flat text payload (the established convention for PHP
1215        // `encapsed_string` / Ruby / C# interpolated strings).
1216        let code = br#"const a = 1; const b = `bare`; const c = `pre ${a} post`;"#;
1217        let root = build_ast::<crate::JavascriptParser>(code, "test.js");
1218        assert_kind_flattened(&root, "template_string");
1219    }
1220
1221    #[cfg(feature = "typescript")]
1222    #[test]
1223    fn typescript_template_string_flattened() {
1224        let code = br#"const a = 1; const b = `bare`; const c = `pre ${a} post`;"#;
1225        let root = build_ast::<crate::TypescriptParser>(code, "test.ts");
1226        assert_kind_flattened(&root, "template_string");
1227    }
1228
1229    #[cfg(feature = "typescript")]
1230    #[test]
1231    fn tsx_template_string_flattened() {
1232        let code = br#"const a = 1; const b = `bare`; const c = `pre ${a} post`;"#;
1233        let root = build_ast::<crate::TsxParser>(code, "test.tsx");
1234        assert_kind_flattened(&root, "template_string");
1235    }
1236
1237    #[cfg(feature = "mozjs")]
1238    #[test]
1239    fn mozjs_template_string_flattened() {
1240        // The MozJS arm's comment previously claimed `TemplateString`
1241        // flattening it did not perform; #699 made the code match.
1242        let code = br#"const a = 1; const b = `bare`; const c = `pre ${a} post`;"#;
1243        let root = build_ast::<crate::MozjsParser>(code, "test.jsm");
1244        assert_kind_flattened(&root, "template_string");
1245    }
1246
1247    #[cfg(feature = "cpp")]
1248    #[test]
1249    fn cpp_concatenated_string_flattened() {
1250        // #699: `is_string` matches `concatenated_string` (`"a" "b"`);
1251        // the alterator now flattens the wrapper instead of leaving its
1252        // adjacent `string_literal` children structured.
1253        let code = br#"const char* s = "a" "b";"#;
1254        let root = build_ast::<crate::CppParser>(code, "test.cpp");
1255        assert_kind_flattened(&root, "concatenated_string");
1256    }
1257
1258    #[cfg(feature = "python")]
1259    #[test]
1260    fn python_string_and_concatenated_string_flattened() {
1261        // #699: Python had no alterator override, so `string` (incl.
1262        // f-strings) and `concatenated_string` kept structured children
1263        // while `is_string` matched them. The override now flattens both;
1264        // the f-string `{a}` interpolation collapses into the text value.
1265        let code = b"a = 1\nb = \"plain\"\nc = f\"x{a}y\"\nd = \"ab\" \"cd\"\n";
1266        let root = build_ast::<crate::PythonParser>(code, "test.py");
1267        assert_kind_flattened(&root, "string");
1268        assert_kind_flattened(&root, "concatenated_string");
1269    }
1270
1271    #[cfg(feature = "java")]
1272    #[test]
1273    fn java_string_literals_flattened() {
1274        // #699: Java had no alterator override, so `string_literal` kept
1275        // its `string_fragment` children. The override flattens it now.
1276        // Triple-quoted text blocks (`"""…"""`) surface as multi-row
1277        // `string_literal` nodes too (the `Java::MultilineStringLiteral`
1278        // enum variant is the hidden `_multiline_string_literal` supertype
1279        // and never appears concretely — see the checker drift marker in
1280        // `simple_is_string_macro_recognises_each_language`); the arm
1281        // lists it for defensive parity with `is_string` but the concrete
1282        // coverage rides on `string_literal`.
1283        let code = b"class T { void m() { String a = \"hi\"; String b = \"\"\"\nblock\"\"\"; } }";
1284        let root = build_ast::<crate::JavaParser>(code, "T.java");
1285        assert_kind_flattened(&root, "string_literal");
1286        // The text block produces a multi-row `string_literal`; confirm
1287        // the flattened node still carries the verbatim multi-line body.
1288        let mut strings = Vec::new();
1289        collect_nodes_by_kind(&root, "string_literal", &mut strings);
1290        assert!(
1291            strings.iter().any(|n| n.value.contains("block")),
1292            "expected the flattened text-block string_literal to keep its body"
1293        );
1294    }
1295
1296    #[cfg(feature = "kotlin")]
1297    #[test]
1298    fn kotlin_string_literals_flattened() {
1299        // #699: Kotlin had no alterator override. `string_literal` (incl.
1300        // `${…}` interpolation, collapsed into the text value) and
1301        // `multiline_string_literal` (`"""…"""`) now flatten.
1302        let code = b"fun m() { val a = 1; val b = \"x${a}y\"; val c = \"\"\"block\"\"\" }";
1303        let root = build_ast::<crate::KotlinParser>(code, "test.kt");
1304        assert_kind_flattened(&root, "string_literal");
1305        assert_kind_flattened(&root, "multiline_string_literal");
1306    }
1307
1308    #[cfg(feature = "go")]
1309    #[test]
1310    fn go_rune_literal_flattened_but_not_a_string_kind() {
1311        // #699 verdict: Go `rune_literal` is operand + flattened but
1312        // deliberately excluded from `is_string` (a rune is a character,
1313        // not a string) — mirroring Rust/Cpp `char_literal`. This pins the
1314        // flattening half; the `is_string` exclusion is pinned in
1315        // `checker.rs::go_rune_literal_is_not_a_string`.
1316        let code = b"package main\nfunc main() { r := 'x'; _ = r }\n";
1317        let root = build_ast::<crate::GoParser>(code, "test.go");
1318        assert_kind_flattened(&root, "rune_literal");
1319    }
1320
1321    #[cfg(feature = "bash")]
1322    #[test]
1323    fn bash_heredoc_body_flattened() {
1324        // #761: `Checker::is_string` matches Bash `heredoc_body`
1325        // (`Bash::HeredocBody2`) but the alterator omitted it, so a
1326        // heredoc body with `${…}` interpolation kept its structured
1327        // `expansion` children in the AST dump while `is_string` treated
1328        // the whole node as one string. The arm now flattens it — same
1329        // convention as Ruby `heredoc_body` / PHP `heredoc`.
1330        let code = b"x=1\ncat <<EOF\npre ${x} post\nEOF\n";
1331        let root = build_ast::<crate::BashParser>(code, "test.sh");
1332        assert_kind_flattened(&root, "heredoc_body");
1333        // The flattened body must carry the verbatim interpolated text,
1334        // not just the literal fragments.
1335        let mut bodies = Vec::new();
1336        collect_nodes_by_kind(&root, "heredoc_body", &mut bodies);
1337        assert!(
1338            bodies.iter().any(|n| n.value.contains("${x}")),
1339            "expected the flattened heredoc_body to keep its interpolation text; got {:?}",
1340            bodies.iter().map(|n| &n.value).collect::<Vec<_>>()
1341        );
1342    }
1343
1344    #[cfg(feature = "perl")]
1345    #[test]
1346    fn perl_heredoc_body_statement_flattened() {
1347        // #761: `Checker::is_string` matches Perl `heredoc_body_statement`
1348        // (`Perl::HeredocBodyStatement`) but the alterator omitted it, so a
1349        // heredoc body with interpolation kept its structured
1350        // `interpolation` children in the AST dump while `is_string`
1351        // treated the whole node as one string. The arm now flattens it.
1352        let code = b"my $x = 1;\nmy $s = <<\"EOF\";\npre $x post\nEOF\n";
1353        let root = build_ast::<crate::PerlParser>(code, "test.pl");
1354        assert_kind_flattened(&root, "heredoc_body_statement");
1355        let mut bodies = Vec::new();
1356        collect_nodes_by_kind(&root, "heredoc_body_statement", &mut bodies);
1357        assert!(
1358            bodies.iter().any(|n| n.value.contains("$x")),
1359            "expected the flattened heredoc_body_statement to keep its interpolation text; got {:?}",
1360            bodies.iter().map(|n| &n.value).collect::<Vec<_>>()
1361        );
1362    }
1363}