Skip to main content

perl_dap/inline_values/
mod.rs

1//! Inline value extraction for DAP inlineValues requests.
2//!
3//! This module provides both a lightweight regex-based implementation and
4//! a runtime-enriched version that queries the Perl debugger for actual
5//! variable values.
6
7use std::collections::{HashMap, HashSet};
8
9mod code_mask;
10mod regex_support;
11
12use code_mask::code_byte_mask;
13use regex_support::{BRACED_PERL_VAR_RE, PERL_VAR_RE, SCALAR_VAR_RE, is_special_variable_name};
14
15use crate::protocol::InlineValueText;
16
17/// Convert 1-based line bounds into inclusive 0-based indexes.
18///
19/// Non-positive line inputs are clamped to line 1. End bounds past the
20/// available line count are clamped to the final line.
21fn normalize_line_bounds(
22    start_line: i64,
23    end_line: i64,
24    line_count: usize,
25) -> Option<(usize, usize)> {
26    if line_count == 0 {
27        return None;
28    }
29
30    let start_1_based = start_line.max(1) as usize;
31    if start_1_based > line_count {
32        return None;
33    }
34    let end_1_based = (end_line.max(1) as usize).min(line_count);
35
36    let start_idx = start_1_based.saturating_sub(1);
37    let end_idx = end_1_based.saturating_sub(1);
38
39    (start_idx <= end_idx).then_some((start_idx, end_idx))
40}
41
42fn collect_line_variables(line: &str, include_non_scalars: bool) -> Vec<(usize, usize, String)> {
43    let mut matches = Vec::new();
44
45    let base_re = if include_non_scalars { PERL_VAR_RE.as_ref() } else { SCALAR_VAR_RE.as_ref() };
46    if let Some(re) = base_re {
47        for cap in re.captures_iter(line) {
48            if let Some(m) = cap.iter().flatten().next() {
49                matches.push((m.start(), m.end(), m.as_str().to_string()));
50            }
51        }
52    }
53
54    if let Some(re) = BRACED_PERL_VAR_RE.as_ref() {
55        for cap in re.captures_iter(line) {
56            let (Some(full_match), Some(sigil_match), Some(name_match)) =
57                (cap.iter().flatten().next(), cap.get(1), cap.get(2))
58            else {
59                continue;
60            };
61            if !include_non_scalars && sigil_match.as_str() != "$" {
62                continue;
63            }
64            matches.push((
65                full_match.start(),
66                full_match.end(),
67                format!("{}{}", sigil_match.as_str(), name_match.as_str()),
68            ));
69        }
70    }
71
72    matches.sort_by(|a, b| (a.0, a.1, &a.2).cmp(&(b.0, b.1, &b.2)));
73    matches.dedup_by(|a, b| a.0 == b.0 && a.1 == b.1 && a.2 == b.2);
74    matches
75}
76
77/// Extract unique variable names from source code within a line range.
78///
79/// Lines are 1-based. Returns deduplicated variable names with their sigils.
80pub fn extract_variable_names(source: &str, start_line: i64, end_line: i64) -> Vec<String> {
81    if PERL_VAR_RE.is_none() && BRACED_PERL_VAR_RE.is_none() {
82        return Vec::new();
83    }
84    let lines: Vec<&str> = source.lines().collect();
85    if lines.is_empty() {
86        return Vec::new();
87    }
88
89    let Some((start_idx, end_idx)) = normalize_line_bounds(start_line, end_line, lines.len())
90    else {
91        return Vec::new();
92    };
93
94    let mut seen = HashSet::new();
95    let mut names = Vec::new();
96
97    for line in lines.iter().skip(start_idx).take(end_idx - start_idx + 1) {
98        let code_mask = code_byte_mask(line);
99        for (start, end, name) in collect_line_variables(line, true) {
100            if !code_mask[start..end].iter().all(|is_code| *is_code) {
101                continue;
102            }
103            if !is_special_variable_name(&name) && seen.insert(name.clone()) {
104                names.push(name);
105            }
106        }
107    }
108
109    names
110}
111
112/// Format a variable's inline value with Perl-idiomatic formatting.
113///
114/// - Scalars: `$x = "value"`
115/// - Arrays: `@arr = (3 elements)`
116/// - Hashes: `%hash = (5 keys)`
117/// - Blessed refs: `$obj = Foo=HASH(...)`
118pub fn format_inline_value(name: &str, raw_value: &str) -> String {
119    let sigil = name.chars().next().unwrap_or('$');
120    match sigil {
121        '@' => {
122            let count = parse_array_element_count(raw_value);
123            format!("{name} = ({count} elements)")
124        }
125        '%' => {
126            let count = parse_hash_key_count(raw_value);
127            format!("{name} = ({count} keys)")
128        }
129        _ => {
130            let trimmed = raw_value.trim();
131            if trimmed.len() > 60 {
132                let preview: String = trimmed.chars().take(57).collect();
133                format!("{name} = {}...", preview)
134            } else {
135                format!("{name} = {trimmed}")
136            }
137        }
138    }
139}
140
141/// Parse an array element count from a debugger response.
142fn parse_array_element_count(raw: &str) -> &str {
143    let trimmed = raw.trim();
144    if trimmed.chars().all(|c| c.is_ascii_digit()) {
145        return trimmed;
146    }
147    "?"
148}
149
150/// Parse a hash key count from a debugger response.
151fn parse_hash_key_count(raw: &str) -> &str {
152    let trimmed = raw.trim();
153    if trimmed.chars().all(|c| c.is_ascii_digit()) {
154        return trimmed;
155    }
156    "?"
157}
158
159/// Collect inline values with runtime variable resolution.
160///
161/// When `runtime_values` is provided, variables are displayed with their
162/// actual values from the debugger. Otherwise, a `= ?` placeholder is used.
163///
164/// Lines and columns are 1-based to match the DAP defaults.
165pub fn collect_inline_values_with_runtime(
166    source: &str,
167    start_line: i64,
168    end_line: i64,
169    runtime_values: Option<&HashMap<String, String>>,
170) -> Vec<InlineValueText> {
171    if PERL_VAR_RE.is_none() && BRACED_PERL_VAR_RE.is_none() {
172        return Vec::new();
173    }
174    let lines: Vec<&str> = source.lines().collect();
175    if lines.is_empty() {
176        return Vec::new();
177    }
178
179    let Some((start_idx, end_idx)) = normalize_line_bounds(start_line, end_line, lines.len())
180    else {
181        return Vec::new();
182    };
183
184    let mut inline_values = Vec::new();
185    let mut seen_on_line: HashSet<(usize, String)> = HashSet::new();
186
187    for (idx, line) in lines.iter().enumerate().skip(start_idx).take(end_idx - start_idx + 1) {
188        let code_mask = code_byte_mask(line);
189        for (start, end, var_name) in collect_line_variables(line, true) {
190            if !code_mask[start..end].iter().all(|is_code| *is_code) {
191                continue;
192            }
193            if is_special_variable_name(&var_name) {
194                continue;
195            }
196            if !seen_on_line.insert((idx, var_name.clone())) {
197                continue;
198            }
199            let column = (start + 1) as i64;
200            let text = match runtime_values.and_then(|rv| rv.get(&var_name)) {
201                Some(rv) => format_inline_value(&var_name, rv),
202                None => format!("{} = ?", var_name),
203            };
204            inline_values.push(InlineValueText { line: (idx + 1) as i64, column, text });
205        }
206    }
207
208    inline_values
209}
210
211/// Legacy: Collect inline values for scalar variables within a line range.
212///
213/// Lines and columns are 1-based to match the DAP defaults.
214/// Scalar-only counterpart to [`collect_inline_values_with_runtime`] that emits
215/// `= ?` placeholders; retained as part of the public inline-values API.
216pub fn collect_inline_values(source: &str, start_line: i64, end_line: i64) -> Vec<InlineValueText> {
217    let lines: Vec<&str> = source.lines().collect();
218    if lines.is_empty() {
219        return Vec::new();
220    }
221
222    let Some((start_idx, end_idx)) = normalize_line_bounds(start_line, end_line, lines.len())
223    else {
224        return Vec::new();
225    };
226
227    if SCALAR_VAR_RE.is_none() && BRACED_PERL_VAR_RE.is_none() {
228        return Vec::new();
229    }
230    let mut inline_values = Vec::new();
231    let mut seen_on_line: HashSet<(usize, String)> = HashSet::new();
232
233    for (idx, line) in lines.iter().enumerate().skip(start_idx).take(end_idx - start_idx + 1) {
234        let code_mask = code_byte_mask(line);
235        for (start, end, var_name) in collect_line_variables(line, false) {
236            if !code_mask[start..end].iter().all(|is_code| *is_code) {
237                continue;
238            }
239            if is_special_variable_name(&var_name) {
240                continue;
241            }
242            if !seen_on_line.insert((idx, var_name.clone())) {
243                continue;
244            }
245            let column = (start + 1) as i64;
246            inline_values.push(InlineValueText {
247                line: (idx + 1) as i64,
248                column,
249                text: format!("{} = ?", var_name),
250            });
251        }
252    }
253
254    inline_values
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn test_inline_value_regexes_compile() {
263        assert!(PERL_VAR_RE.is_some());
264        assert!(BRACED_PERL_VAR_RE.is_some());
265        assert!(SCALAR_VAR_RE.is_some());
266    }
267
268    #[test]
269    fn test_collect_line_variables_keeps_code_scalars_visible() {
270        let line = "my $x = 1;";
271        let matches = collect_line_variables(line, false);
272        assert_eq!(matches, vec![(3, 5, "$x".to_string())]);
273
274        let code_mask = code_byte_mask(line);
275        assert!(code_mask[3..5].iter().all(|is_code| *is_code));
276    }
277
278    #[test]
279    fn test_collect_inline_values_legacy() {
280        let source = "my $x = 1;\nmy $y = $x + 2;";
281        let values = collect_inline_values(source, 1, 2);
282        assert!(values.iter().any(|v| v.text.contains("$x")));
283        assert!(values.iter().any(|v| v.text.contains("$y")));
284    }
285
286    #[test]
287    fn test_scalar_inline_value() {
288        let source = "my $name = \"Hello\";";
289        let mut rv = HashMap::new();
290        rv.insert("$name".to_string(), "'Hello'".to_string());
291        let values = collect_inline_values_with_runtime(source, 1, 1, Some(&rv));
292        assert_eq!(values.len(), 1);
293        assert_eq!(values[0].text, "$name = 'Hello'");
294    }
295
296    #[test]
297    fn test_array_inline_value() {
298        let source = "my @items = (1, 2, 3);";
299        let mut rv = HashMap::new();
300        rv.insert("@items".to_string(), "3".to_string());
301        let values = collect_inline_values_with_runtime(source, 1, 1, Some(&rv));
302        assert_eq!(values[0].text, "@items = (3 elements)");
303    }
304
305    #[test]
306    fn test_hash_inline_value() {
307        let source = "my %config = (a => 1);";
308        let mut rv = HashMap::new();
309        rv.insert("%config".to_string(), "5".to_string());
310        let values = collect_inline_values_with_runtime(source, 1, 1, Some(&rv));
311        assert_eq!(values[0].text, "%config = (5 keys)");
312    }
313
314    #[test]
315    fn test_blessed_ref_inline_value() {
316        let source = "my $obj = Foo->new();";
317        let mut rv = HashMap::new();
318        rv.insert("$obj".to_string(), "Foo=HASH(0xdeadbeef)".to_string());
319        let values = collect_inline_values_with_runtime(source, 1, 1, Some(&rv));
320        assert_eq!(values[0].text, "$obj = Foo=HASH(0xdeadbeef)");
321    }
322
323    #[test]
324    fn test_empty_collections() {
325        let mut rv = HashMap::new();
326        rv.insert("@empty".to_string(), "0".to_string());
327        rv.insert("%none".to_string(), "0".to_string());
328        let source = "my @empty; my %none;";
329        let values = collect_inline_values_with_runtime(source, 1, 1, Some(&rv));
330        assert!(values.iter().any(|v| v.text == "@empty = (0 elements)"));
331        assert!(values.iter().any(|v| v.text == "%none = (0 keys)"));
332    }
333
334    #[test]
335    fn test_deduplication_per_line() {
336        let source = "$x = $x + $x;";
337        let values = collect_inline_values_with_runtime(source, 1, 2, None);
338        assert_eq!(values.len(), 1);
339    }
340
341    #[test]
342    fn test_special_vars_excluded() {
343        let source = "print $_; warn $!; my $val = 1;";
344        let values = collect_inline_values_with_runtime(source, 1, 1, None);
345        assert_eq!(values.len(), 1);
346        assert!(values[0].text.contains("$val"));
347    }
348
349    #[test]
350    fn test_extract_variable_names() {
351        let source = "my $x = 1;\nmy @arr = (1,2,3);\nmy %h = (a => 1);";
352        let names = extract_variable_names(source, 1, 3);
353        assert!(names.contains(&"$x".to_string()));
354        assert!(names.contains(&"@arr".to_string()));
355        assert!(names.contains(&"%h".to_string()));
356    }
357
358    #[test]
359    fn test_extract_variable_names_with_namespace_qualifiers() {
360        let source = "our $Foo::bar = 1;\nour @My::Pkg::items = (1);\nour %App::Config::opts = ();";
361        let names = extract_variable_names(source, 1, 3);
362        assert!(names.contains(&"$Foo::bar".to_string()));
363        assert!(names.contains(&"@My::Pkg::items".to_string()));
364        assert!(names.contains(&"%App::Config::opts".to_string()));
365    }
366
367    #[test]
368    fn test_extract_variable_names_with_legacy_namespace_qualifiers() {
369        let source = "our $Foo'bar = 1;\nour @My'Pkg'items = (1);\nour %App'Config'opts = ();";
370        let names = extract_variable_names(source, 1, 3);
371        assert!(names.contains(&"$Foo'bar".to_string()));
372        assert!(names.contains(&"@My'Pkg'items".to_string()));
373        assert!(names.contains(&"%App'Config'opts".to_string()));
374    }
375
376    #[test]
377    fn test_no_runtime_fallback() {
378        let source = "my $x = 1;";
379        let values = collect_inline_values_with_runtime(source, 1, 1, None);
380        assert_eq!(values.len(), 1);
381        assert_eq!(values[0].text, "$x = ?");
382    }
383
384    #[test]
385    fn test_scalar_truncation_uses_char_boundaries() {
386        let source = "my $name = 1;";
387        let long_value = "é".repeat(80);
388        let mut rv = HashMap::new();
389        rv.insert("$name".to_string(), long_value);
390
391        let values = collect_inline_values_with_runtime(source, 1, 1, Some(&rv));
392        assert_eq!(values.len(), 1);
393        assert!(values[0].text.starts_with("$name = "));
394        assert!(values[0].text.ends_with("..."));
395    }
396
397    #[test]
398    fn test_non_positive_line_bounds_are_clamped() {
399        let source = "my $x = 1;\nmy $y = 2;";
400        let names = extract_variable_names(source, 0, 1);
401        assert_eq!(names, vec!["$x".to_string()]);
402
403        let values = collect_inline_values_with_runtime(source, 0, 0, None);
404        assert_eq!(values.len(), 1);
405        assert_eq!(values[0].text, "$x = ?");
406    }
407
408    #[test]
409    fn test_inverted_line_bounds_return_empty() {
410        let source = "my $x = 1;\nmy $y = 2;";
411        assert!(extract_variable_names(source, 2, 1).is_empty());
412        assert!(collect_inline_values_with_runtime(source, 2, 1, None).is_empty());
413        assert!(collect_inline_values(source, 2, 1).is_empty());
414    }
415
416    #[test]
417    fn test_out_of_range_line_bounds_return_empty() {
418        let source = "my $x = 1;\nmy $y = 2;";
419        assert!(extract_variable_names(source, 3, 3).is_empty());
420        assert!(collect_inline_values_with_runtime(source, 3, 3, None).is_empty());
421        assert!(collect_inline_values(source, 3, 3).is_empty());
422    }
423
424    #[test]
425    fn test_end_line_past_file_is_clamped() {
426        let source = "my $x = 1;\nmy $y = 2;";
427
428        let names = extract_variable_names(source, 2, 999);
429        assert_eq!(names, vec!["$y".to_string()]);
430
431        let values = collect_inline_values_with_runtime(source, 2, 999, None);
432        assert_eq!(values.len(), 1);
433        assert_eq!(values[0].text, "$y = ?");
434
435        let legacy_values = collect_inline_values(source, 2, 999);
436        assert_eq!(legacy_values.len(), 1);
437        assert_eq!(legacy_values[0].text, "$y = ?");
438    }
439
440    #[test]
441    fn test_runtime_inline_value_for_namespaced_scalar() {
442        let source = "our $Foo::bar = 1;";
443        let mut rv = HashMap::new();
444        rv.insert("$Foo::bar".to_string(), "42".to_string());
445
446        let values = collect_inline_values_with_runtime(source, 1, 1, Some(&rv));
447        assert_eq!(values.len(), 1);
448        assert_eq!(values[0].text, "$Foo::bar = 42");
449    }
450
451    #[test]
452    fn test_runtime_inline_value_for_legacy_namespaced_scalar() {
453        let source = "our $Foo'bar = 1;";
454        let mut rv = HashMap::new();
455        rv.insert("$Foo'bar".to_string(), "42".to_string());
456
457        let values = collect_inline_values_with_runtime(source, 1, 1, Some(&rv));
458        assert_eq!(values.len(), 1);
459        assert_eq!(values[0].text, "$Foo'bar = 42");
460    }
461
462    #[test]
463    fn test_runtime_inline_value_for_main_namespaced_scalar() {
464        let source = "our $::bar = 1;";
465        let mut rv = HashMap::new();
466        rv.insert("$::bar".to_string(), "42".to_string());
467
468        let values = collect_inline_values_with_runtime(source, 1, 1, Some(&rv));
469        assert_eq!(values.len(), 1);
470        assert_eq!(values[0].text, "$::bar = 42");
471    }
472
473    #[test]
474    fn test_variable_like_tokens_in_strings_and_comments_are_ignored() {
475        let source = "my $real = 1; my $msg = \"$fake\"; # $commented";
476        let names = extract_variable_names(source, 1, 1);
477        assert!(names.contains(&"$real".to_string()));
478        assert!(names.contains(&"$msg".to_string()));
479        assert!(!names.contains(&"$fake".to_string()));
480        assert!(!names.contains(&"$commented".to_string()));
481    }
482
483    #[test]
484    fn test_inline_values_ignore_strings_and_comments() {
485        let source = "my $real = 1; print \"$ignored\"; # and $commented";
486        let values = collect_inline_values_with_runtime(source, 1, 1, None);
487        assert_eq!(values.len(), 1);
488        assert_eq!(values[0].text, "$real = ?");
489    }
490
491    #[test]
492    fn test_array_length_marker_does_not_start_comment() {
493        let source = "my $len = $#arr; my $next = 1;";
494
495        let names = extract_variable_names(source, 1, 1);
496        assert!(names.contains(&"$len".to_string()));
497        assert!(names.contains(&"$next".to_string()));
498
499        let values = collect_inline_values_with_runtime(source, 1, 1, None);
500        assert!(values.iter().any(|v| v.text == "$len = ?"));
501        assert!(values.iter().any(|v| v.text == "$next = ?"));
502    }
503
504    #[test]
505    fn test_braced_array_length_marker_does_not_start_comment() {
506        let source = "my $len = ${#arr}; my $next = 1;";
507
508        let names = extract_variable_names(source, 1, 1);
509        assert!(names.contains(&"$len".to_string()));
510        assert!(names.contains(&"$next".to_string()));
511    }
512
513    #[test]
514    fn test_variable_like_tokens_in_quote_like_operators_are_ignored() {
515        let source = "my $real = 1; my $str = qq{$fake}; my $lit = q[$ghost];";
516        let names = extract_variable_names(source, 1, 1);
517        assert!(names.contains(&"$real".to_string()));
518        assert!(names.contains(&"$str".to_string()));
519        assert!(names.contains(&"$lit".to_string()));
520        assert!(!names.contains(&"$fake".to_string()));
521        assert!(!names.contains(&"$ghost".to_string()));
522    }
523
524    #[test]
525    fn test_inline_values_ignore_quote_like_operators() {
526        let source = "my $real = 1; my $str = qq($fake); my $lit = q/$ghost/;";
527        let values = collect_inline_values_with_runtime(source, 1, 1, None);
528        assert_eq!(values.len(), 3);
529        assert!(values.iter().any(|v| v.text == "$real = ?"));
530        assert!(values.iter().any(|v| v.text == "$str = ?"));
531        assert!(values.iter().any(|v| v.text == "$lit = ?"));
532        assert!(!values.iter().any(|v| v.text.contains("$fake")));
533        assert!(!values.iter().any(|v| v.text.contains("$ghost")));
534    }
535
536    #[test]
537    fn test_unclosed_quote_like_operator_masks_to_end_of_line() {
538        let source = "my $real = 1; my $str = qq($fake";
539        let values = collect_inline_values_with_runtime(source, 1, 1, None);
540        assert_eq!(values.len(), 2);
541        assert!(values.iter().any(|v| v.text == "$real = ?"));
542        assert!(values.iter().any(|v| v.text == "$str = ?"));
543        assert!(!values.iter().any(|v| v.text.contains("$fake")));
544    }
545
546    #[test]
547    fn test_regex_and_translation_operators_are_ignored() {
548        let source =
549            "my $real = 1; $text =~ m/$ghost/; $text =~ s/$from/$to/; $text =~ tr/$src/$dst/;";
550        let names = extract_variable_names(source, 1, 1);
551        assert!(names.contains(&"$real".to_string()));
552        assert!(!names.contains(&"$ghost".to_string()));
553        assert!(!names.contains(&"$from".to_string()));
554        assert!(!names.contains(&"$to".to_string()));
555        assert!(!names.contains(&"$src".to_string()));
556        assert!(!names.contains(&"$dst".to_string()));
557    }
558
559    #[test]
560    fn test_quote_like_parser_requires_identifier_boundary() {
561        let source = "my $keep = 1; my $weird = qqx$also_keep;";
562        let names = extract_variable_names(source, 1, 1);
563        assert!(names.contains(&"$keep".to_string()));
564        assert!(names.contains(&"$weird".to_string()));
565        assert!(names.contains(&"$also_keep".to_string()));
566    }
567
568    #[test]
569    fn test_extract_variable_names_deduplicates_repeated_mentions() {
570        let source = "my $foo = ${foo};\n";
571        let names = extract_variable_names(source, 1, 1);
572
573        assert_eq!(names, vec!["$foo".to_string()]);
574    }
575
576    #[test]
577    fn test_extract_variable_names_supports_braced_variables() {
578        let source = "my ${scalar_name} = 1;\nmy @{Pkg::items};\nmy %{App::Config::opts};";
579        let names = extract_variable_names(source, 1, 3);
580        assert!(names.contains(&"$scalar_name".to_string()));
581        assert!(names.contains(&"@Pkg::items".to_string()));
582        assert!(names.contains(&"%App::Config::opts".to_string()));
583    }
584
585    #[test]
586    fn test_inline_values_support_braced_scalars_with_runtime_values() {
587        let source = "my ${scalar_name} = 1;";
588        let mut rv = HashMap::new();
589        rv.insert("$scalar_name".to_string(), "7".to_string());
590        let values = collect_inline_values_with_runtime(source, 1, 1, Some(&rv));
591        assert_eq!(values.len(), 1);
592        assert_eq!(values[0].text, "$scalar_name = 7");
593    }
594
595    #[test]
596    fn test_legacy_inline_values_support_braced_scalars() {
597        let source = "my ${scalar_name} = 1;";
598        let values = collect_inline_values(source, 1, 1);
599        assert_eq!(values.len(), 1);
600        assert_eq!(values[0].text, "$scalar_name = ?");
601    }
602
603    #[test]
604    fn test_legacy_inline_values_deduplicate_per_line() {
605        let source = "$x = $x + $x;";
606        let values = collect_inline_values(source, 1, 1);
607        assert_eq!(values.len(), 1);
608        assert_eq!(values[0].text, "$x = ?");
609    }
610
611    #[test]
612    fn test_legacy_inline_values_exclude_special_variables() {
613        let source = "print $_; warn $!; my $ok = 1;";
614        let values = collect_inline_values(source, 1, 1);
615        assert_eq!(values.len(), 1);
616        assert_eq!(values[0].text, "$ok = ?");
617    }
618
619    #[test]
620    fn test_inline_values_ignore_regex_match_operator_body() {
621        let source = r"my $target = 1; if ($line =~ m/(\$regex_capture)/) { my $ok = 1; }";
622        let values = collect_inline_values_with_runtime(source, 1, 1, None);
623        assert!(values.iter().any(|v| v.text == "$target = ?"));
624        assert!(values.iter().any(|v| v.text == "$line = ?"));
625        assert!(values.iter().any(|v| v.text == "$ok = ?"));
626        assert!(!values.iter().any(|v| v.text.contains("$regex_capture")));
627    }
628
629    #[test]
630    fn test_inline_values_ignore_substitution_and_transliteration_bodies() {
631        let source = "my $line = 1; $line =~ s/$find/$replace/g; $line =~ tr/$from/$to/;";
632        let values = collect_inline_values_with_runtime(source, 1, 1, None);
633        assert!(values.iter().any(|v| v.text == "$line = ?"));
634        assert!(!values.iter().any(|v| v.text.contains("$find")));
635        assert!(!values.iter().any(|v| v.text.contains("$replace")));
636        assert!(!values.iter().any(|v| v.text.contains("$from")));
637        assert!(!values.iter().any(|v| v.text.contains("$to")));
638    }
639}