Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/yamlfix/adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@

log = logging.getLogger(__name__)

# Pattern matching escaped non-BMP unicode sequences produced by ruyaml's emitter.
# ruyaml (v0.91.0) only allows characters up to U+FFFD, escaping supplementary
# plane characters (emojis, etc.) as \Uxxxxxxxx. This is fixed on ruyaml's main
# branch but not yet released to PyPI.
_UNICODE_ESCAPE_RE = re.compile(r"[\\]U([0-9A-Fa-f]{8})")


class Yaml:
"""Adapter that holds the configured ruaml yaml fixer."""
Expand Down Expand Up @@ -352,6 +358,7 @@ def fix(self, source_code: str) -> str:
self._restore_truthy_strings,
self._restore_jinja_variables,
self._restore_double_exclamations,
self._restore_unicode_escapes,
self._fix_comments,
self._fix_flow_style_lists,
self._fix_whitelines,
Expand Down Expand Up @@ -718,6 +725,17 @@ def _restore_double_exclamations(source_code: str) -> str:

return "\n".join(fixed_source_lines)

@staticmethod
def _restore_unicode_escapes(source_code: str) -> str:
r"""Restore escaped non-BMP unicode sequences to their original characters.

The ruyaml emitter (v0.91.0) escapes supplementary plane characters
(U+10000 to U+10FFFF, including emojis) in double-quoted strings as
\Uxxxxxxxx sequences. This converts them back to the actual characters.
"""
log.debug("Restoring unicode escapes...")
return _UNICODE_ESCAPE_RE.sub(lambda m: chr(int(m.group(1), 16)), source_code)

@staticmethod
def _add_newline_at_end_of_file(source_code: str) -> str:
"""Ensures that the file ends with exactly one newline.
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,7 @@ def test_fix_code_functions_emit_debug_logs(
"Restoring truthy strings...",
"Restoring jinja2 variables...",
"Restoring double exclamations...",
"Restoring unicode escapes...",
"Fixing comments...",
"Fixing top level lists...",
"Fixing flow-style lists...",
Expand Down Expand Up @@ -619,6 +620,33 @@ def test_fix_code_respects_comment_symbol_in_strings_with_double_quotes(

assert result == desired_source

def test_fix_code_preserves_unicode_emojis_in_double_quoted_strings(
self,
) -> None:
r"""
Given: Code with emojis (non-BMP unicode) in double-quoted strings
When: fix_code is run with preserve_quotes=True
Then: The emojis are preserved, not escaped to \Uxxxxxxxx sequences

Regression test for https://github.com/lyz-code/yamlfix/issues/300
"""
source = dedent(
"""\
---
stages:
- "🔬 checks"
- "🚀 Release"
"""
)
config = YamlfixConfig()
config.preserve_quotes = True

result = fix_code(source, config)

assert "🔬" in result
assert "🚀" in result
assert "\\U" not in result

def test_fix_code_respects_jinja_variables_with_equals(
self,
) -> None:
Expand Down
Loading