diff --git a/ci/string_literals_stress_test.rb b/ci/string_literals_stress_test.rb index 0a80d9f58..9e9f03442 100644 --- a/ci/string_literals_stress_test.rb +++ b/ci/string_literals_stress_test.rb @@ -66,6 +66,22 @@ @foo = 3 puts '#@foo' puts '#{3}' + +$global = 1 +puts '\#$global' +puts '"\#$global"' + +@ivar = 2 +puts '\#@ivar' +puts '"\#@ivar"' + +local_var = 3 +puts '\#{local_var}' +puts '"\#{local_var}"' +puts %q{text with \#{local_var} test} +puts '\#foo' +puts 'a\#{local_var}c' +puts '\\#{local_var}' puts %q("") puts %q(\"\") puts %Q(\"\") diff --git a/fixtures/small/single_quoted_backslash_hash_no_interpolation_actual.rb b/fixtures/small/single_quoted_backslash_hash_no_interpolation_actual.rb new file mode 100644 index 000000000..0cca13e17 --- /dev/null +++ b/fixtures/small/single_quoted_backslash_hash_no_interpolation_actual.rb @@ -0,0 +1,3 @@ +puts('\#foo') +puts('a\#{b}c') +puts('\\#{x}') diff --git a/fixtures/small/single_quoted_backslash_hash_no_interpolation_expected.rb b/fixtures/small/single_quoted_backslash_hash_no_interpolation_expected.rb new file mode 100644 index 000000000..a1563478e --- /dev/null +++ b/fixtures/small/single_quoted_backslash_hash_no_interpolation_expected.rb @@ -0,0 +1,3 @@ +puts("\\#foo") +puts("a\\\#{b}c") +puts("\\\#{x}") diff --git a/fixtures/small/single_quoted_hash_interpolation_escape_actual.rb b/fixtures/small/single_quoted_hash_interpolation_escape_actual.rb new file mode 100644 index 000000000..ac61257e8 --- /dev/null +++ b/fixtures/small/single_quoted_hash_interpolation_escape_actual.rb @@ -0,0 +1,9 @@ +$g = 1 +puts('"\#$g"') + +@g = 2 +puts('"\#@g"') + +x = 3 +puts('"\#{x}"') +puts(%q{text with \#{x} test}) diff --git a/fixtures/small/single_quoted_hash_interpolation_escape_expected.rb b/fixtures/small/single_quoted_hash_interpolation_escape_expected.rb new file mode 100644 index 000000000..1eb069274 --- /dev/null +++ b/fixtures/small/single_quoted_hash_interpolation_escape_expected.rb @@ -0,0 +1,9 @@ +$g = 1 +puts("\"\\\#$g\"") + +@g = 2 +puts("\"\\\#@g\"") + +x = 3 +puts("\"\\\#{x}\"") +puts("text with \\\#{x} test") diff --git a/librubyfmt/src/string_escape.rs b/librubyfmt/src/string_escape.rs index 81ad03884..5c3f7284a 100644 --- a/librubyfmt/src/string_escape.rs +++ b/librubyfmt/src/string_escape.rs @@ -179,6 +179,17 @@ fn escape_string(content: &[u8], opening_delim: u8, closing_delim: u8) -> Vec { + output.push(b'\\'); + output.push(b'\\'); + continue; + } // For everything else, this is not an escape sequence, so we need to // escape the slash and then print the next character. _ => { @@ -309,6 +320,58 @@ mod tests { ); } + /// In a single-quoted string, a backslash is only special before `\` or `'`. + /// So `\#$g`, `\#@g` and `\#{x}` are just the literal characters `\`, `#`, and + /// whatever follows — there is no interpolation. When converting to a + /// double-quoted string we must preserve that: the backslash needs to be + /// escaped (`\\`) *and* the `#` needs to be escaped (`\#`) so it doesn't turn + /// into real interpolation. + #[test] + fn single_quoted_literal_backslash_hash_dollar_not_interpolated() { + assert_eq!( + single_to_double_quoted(b"\\#$g", b"'", b"'").as_ref(), + b"\\\\\\#$g" + ); + } + + #[test] + fn single_quoted_literal_backslash_hash_at_not_interpolated() { + assert_eq!( + single_to_double_quoted(b"\\#@g", b"'", b"'").as_ref(), + b"\\\\\\#@g" + ); + } + + #[test] + fn single_quoted_literal_backslash_hash_brace_not_interpolated() { + assert_eq!( + single_to_double_quoted(b"\\#{x}", b"'", b"'").as_ref(), + b"\\\\\\#{x}" + ); + } + + #[test] + fn single_quoted_literal_backslash_hash_before_regular_char() { + // \#foo — the backslash needs escaping, but `#foo` isn't an + // interpolation trigger so the `#` stays unescaped. + assert_eq!( + single_to_double_quoted(b"\\#foo", b"'", b"'").as_ref(), + b"\\\\#foo" + ); + } + + #[test] + fn single_quoted_escaped_backslash_then_hash_brace_is_interpolation() { + // '\\#{x}' is an *escaped* backslash followed by `#{x}`, which *is* + // interpolation-worthy in the original single-quoted source too — so + // once converted to double-quoted, the `#` must still be escaped to + // preserve the literal (non-interpolated) meaning. + assert_eq!( + single_to_double_quoted(b"\\\\#{x}", b"'", b"'").as_ref(), + b"\\\\\\#{x}" + ); + } + // --- escape_string with %q(...) strings --- #[test]