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}