From 7627522634d4c1be62009a0433e62cd527d58ea9 Mon Sep 17 00:00:00 2001 From: Mohamed MAACHE Date: Sun, 5 Jul 2026 03:24:12 +0200 Subject: [PATCH] fix: skip comment bodies when tracking quote state for interpolation repair detect_missing_record_comma_positions tracked single- and double-quote string state but had no awareness of `#` line comments. An apostrophe inside a comment (don't, it's, can't) toggled the single-quote tracking just like a real string, desyncing quote parity for everything after it until the next `'`. That could reintroduce the exact #201 spurious-comma bug even with balanced quotes in actual code, as long as a comment with an odd number of apostrophes preceded a $"..." interpolation closely enough to fall in the repair window. Skip `#`-to-end-of-line comment spans before doing byte-level quote tracking, reusing the existing string-aware comment extraction from comments.rs so a `#` inside a string still isn't treated as a comment. Fixes #201 --- src/formatting/repair.rs | 53 ++++++++++++++++++- ...e_does_not_break_interpolation_issue201.nu | 5 ++ ...e_does_not_break_interpolation_issue201.nu | 6 +++ ...t_does_not_break_interpolation_issue201.nu | 5 ++ ...e_does_not_break_interpolation_issue201.nu | 4 ++ ...e_does_not_break_interpolation_issue201.nu | 5 ++ ...e_does_not_break_interpolation_issue201.nu | 6 +++ ...t_does_not_break_interpolation_issue201.nu | 5 ++ ...e_does_not_break_interpolation_issue201.nu | 4 ++ tests/ground_truth.rs | 20 +++++++ 10 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/expected/comment_apostrophe_does_not_break_interpolation_issue201.nu create mode 100644 tests/fixtures/expected/mid_file_comment_apostrophe_does_not_break_interpolation_issue201.nu create mode 100644 tests/fixtures/expected/multiple_apostrophes_in_comment_does_not_break_interpolation_issue201.nu create mode 100644 tests/fixtures/expected/single_quoted_double_quote_does_not_break_interpolation_issue201.nu create mode 100644 tests/fixtures/input/comment_apostrophe_does_not_break_interpolation_issue201.nu create mode 100644 tests/fixtures/input/mid_file_comment_apostrophe_does_not_break_interpolation_issue201.nu create mode 100644 tests/fixtures/input/multiple_apostrophes_in_comment_does_not_break_interpolation_issue201.nu create mode 100644 tests/fixtures/input/single_quoted_double_quote_does_not_break_interpolation_issue201.nu diff --git a/src/formatting/repair.rs b/src/formatting/repair.rs index 7759b97..ae114c5 100644 --- a/src/formatting/repair.rs +++ b/src/formatting/repair.rs @@ -194,18 +194,44 @@ pub(super) fn try_repair_compact_if_else(source: &str) -> (String, bool) { /// Detect byte positions where a comma is likely missing between record fields. fn detect_missing_record_comma_positions(source: &str) -> Vec { let bytes = source.as_bytes(); + // Comments are extracted with their own string-aware scan (see + // `comments::extract_comments`) so a `#` inside a string literal is never + // mistaken for a comment start. We reuse those spans here to skip over + // comment bodies entirely: an apostrophe inside a comment (e.g. "don't", + // "it's") must never be mistaken for the start of a single-quoted string, + // or quote-parity tracking below desyncs for the rest of the file. + let comment_spans = super::comments::extract_comments(bytes); + let mut comments = comment_spans.iter(); + let mut next_comment = comments.next(); + let mut in_string = false; + let mut in_single_string = false; let mut escaped = false; let mut insert_positions: Vec = Vec::new(); - for (idx, &byte) in bytes.iter().enumerate() { + let mut idx = 0; + while idx < bytes.len() { + let byte = bytes[idx]; + + // Single-quoted strings are raw in nushell: no escapes, and a `"` inside + // them must not toggle the double-quote tracking below. + if in_single_string { + if byte == b'\'' { + in_single_string = false; + } + idx += 1; + continue; + } + if in_string { if escaped { escaped = false; + idx += 1; continue; } if byte == b'\\' { escaped = true; + idx += 1; continue; } if byte == b'"' { @@ -237,13 +263,38 @@ fn detect_missing_record_comma_positions(source: &str) -> Vec { } } } + idx += 1; continue; } + // Drop any comment spans we've already passed (defensive; should not + // normally trigger since both scans walk left-to-right in lockstep). + while let Some((span, _)) = next_comment { + if span.end <= idx { + next_comment = comments.next(); + } else { + break; + } + } + + // Not inside any string: a `#` here starts a line comment. Skip its + // whole body so apostrophes/quotes inside it are never tracked. + if let Some((span, _)) = next_comment { + if span.start == idx { + idx = span.end; + next_comment = comments.next(); + continue; + } + } + if byte == b'"' { in_string = true; escaped = false; + } else if byte == b'\'' { + in_single_string = true; } + + idx += 1; } insert_positions diff --git a/tests/fixtures/expected/comment_apostrophe_does_not_break_interpolation_issue201.nu b/tests/fixtures/expected/comment_apostrophe_does_not_break_interpolation_issue201.nu new file mode 100644 index 0000000..c491dbb --- /dev/null +++ b/tests/fixtures/expected/comment_apostrophe_does_not_break_interpolation_issue201.nu @@ -0,0 +1,5 @@ +def main [] { + # don't + let x = '"' + print $"Hello: ($x)" +} diff --git a/tests/fixtures/expected/mid_file_comment_apostrophe_does_not_break_interpolation_issue201.nu b/tests/fixtures/expected/mid_file_comment_apostrophe_does_not_break_interpolation_issue201.nu new file mode 100644 index 0000000..b2d0909 --- /dev/null +++ b/tests/fixtures/expected/mid_file_comment_apostrophe_does_not_break_interpolation_issue201.nu @@ -0,0 +1,6 @@ +def main [] { + # don't + + let x = '"' + print $"Hello: ($x)" +} diff --git a/tests/fixtures/expected/multiple_apostrophes_in_comment_does_not_break_interpolation_issue201.nu b/tests/fixtures/expected/multiple_apostrophes_in_comment_does_not_break_interpolation_issue201.nu new file mode 100644 index 0000000..9189ea0 --- /dev/null +++ b/tests/fixtures/expected/multiple_apostrophes_in_comment_does_not_break_interpolation_issue201.nu @@ -0,0 +1,5 @@ +def main [] { + # it's John's, don't + let x = '"' + print $"Hello: ($x)" +} diff --git a/tests/fixtures/expected/single_quoted_double_quote_does_not_break_interpolation_issue201.nu b/tests/fixtures/expected/single_quoted_double_quote_does_not_break_interpolation_issue201.nu new file mode 100644 index 0000000..0d0a1fa --- /dev/null +++ b/tests/fixtures/expected/single_quoted_double_quote_does_not_break_interpolation_issue201.nu @@ -0,0 +1,4 @@ +def main [] { + let x = '"' + print $"Hello: ($x)" +} diff --git a/tests/fixtures/input/comment_apostrophe_does_not_break_interpolation_issue201.nu b/tests/fixtures/input/comment_apostrophe_does_not_break_interpolation_issue201.nu new file mode 100644 index 0000000..c491dbb --- /dev/null +++ b/tests/fixtures/input/comment_apostrophe_does_not_break_interpolation_issue201.nu @@ -0,0 +1,5 @@ +def main [] { + # don't + let x = '"' + print $"Hello: ($x)" +} diff --git a/tests/fixtures/input/mid_file_comment_apostrophe_does_not_break_interpolation_issue201.nu b/tests/fixtures/input/mid_file_comment_apostrophe_does_not_break_interpolation_issue201.nu new file mode 100644 index 0000000..b2d0909 --- /dev/null +++ b/tests/fixtures/input/mid_file_comment_apostrophe_does_not_break_interpolation_issue201.nu @@ -0,0 +1,6 @@ +def main [] { + # don't + + let x = '"' + print $"Hello: ($x)" +} diff --git a/tests/fixtures/input/multiple_apostrophes_in_comment_does_not_break_interpolation_issue201.nu b/tests/fixtures/input/multiple_apostrophes_in_comment_does_not_break_interpolation_issue201.nu new file mode 100644 index 0000000..9189ea0 --- /dev/null +++ b/tests/fixtures/input/multiple_apostrophes_in_comment_does_not_break_interpolation_issue201.nu @@ -0,0 +1,5 @@ +def main [] { + # it's John's, don't + let x = '"' + print $"Hello: ($x)" +} diff --git a/tests/fixtures/input/single_quoted_double_quote_does_not_break_interpolation_issue201.nu b/tests/fixtures/input/single_quoted_double_quote_does_not_break_interpolation_issue201.nu new file mode 100644 index 0000000..0d0a1fa --- /dev/null +++ b/tests/fixtures/input/single_quoted_double_quote_does_not_break_interpolation_issue201.nu @@ -0,0 +1,4 @@ +def main [] { + let x = '"' + print $"Hello: ($x)" +} diff --git a/tests/ground_truth.rs b/tests/ground_truth.rs index 47f599d..1a97570 100644 --- a/tests/ground_truth.rs +++ b/tests/ground_truth.rs @@ -709,4 +709,24 @@ fixture_tests!( ground_truth_tab_indentation_via_config_issue196, idempotency_tab_indentation_via_config_issue196 ), + ( + "single_quoted_double_quote_does_not_break_interpolation_issue201", + ground_truth_single_quoted_double_quote_does_not_break_interpolation_issue201, + idempotency_single_quoted_double_quote_does_not_break_interpolation_issue201 + ), + ( + "comment_apostrophe_does_not_break_interpolation_issue201", + ground_truth_comment_apostrophe_does_not_break_interpolation_issue201, + idempotency_comment_apostrophe_does_not_break_interpolation_issue201 + ), + ( + "mid_file_comment_apostrophe_does_not_break_interpolation_issue201", + ground_truth_mid_file_comment_apostrophe_does_not_break_interpolation_issue201, + idempotency_mid_file_comment_apostrophe_does_not_break_interpolation_issue201 + ), + ( + "multiple_apostrophes_in_comment_does_not_break_interpolation_issue201", + ground_truth_multiple_apostrophes_in_comment_does_not_break_interpolation_issue201, + idempotency_multiple_apostrophes_in_comment_does_not_break_interpolation_issue201 + ), );