diff --git a/src/mutmut/mutation/mutators.py b/src/mutmut/mutation/mutators.py index d6fa5252..f4ff29c9 100644 --- a/src/mutmut/mutation/mutators.py +++ b/src/mutmut/mutation/mutators.py @@ -19,8 +19,17 @@ ] ] -# pattern to match (nearly) all chars in a string that are not part of an escape sequence -NON_ESCAPE_SEQUENCE = re.compile(r"((? Iterable[cst.BaseNumber]: @@ -53,13 +62,31 @@ def operator_string(node: cst.BaseString) -> Iterable[cst.BaseString]: # that mutation is meaningless for return + # For raw strings (r"..." or R"..."), escape sequences are literal, so case changes are NOT equivalent + is_raw = "r" in prefix.lower() + supported_str_mutations: list[Callable[[str], str]] = [ lambda x: "XX" + x + "XX", - # do not modify escape sequences, as this could break python syntax - lambda x: NON_ESCAPE_SEQUENCE.sub(lambda match: match.group(1).lower(), x), - lambda x: NON_ESCAPE_SEQUENCE.sub(lambda match: match.group(1).upper(), x), ] + if is_raw: + # Raw strings: use original regex (all non-backslash chars) + old_pattern = re.compile(r"((? str: r'a: str = "Fo\N{ghost} \U11223344"', [ r'a: str = "XXFo\N{ghost} \U11223344XX"', - r'a: str = "FO\N{GHOST} \U11223344"', + r'a: str = "FO\N{ghost} \U11223344"', r'a: str = "fo\N{ghost} \U11223344"', "a: str = None", ], ), + # Escape sequences with hex digits should not produce equivalent case-flip mutants + (r'"\u00a0"', [r'"XX\u00a0XX"']), + (r'"\U0001f600"', [r'"XX\U0001f600XX"']), + (r'"\x7f"', [r'"XX\x7fXX"']), + (r'"\N{GREEK CAPITAL LETTER DELTA}"', [r'"XX\N{GREEK CAPITAL LETTER DELTA}XX"']), + (r'"Hello\u00a0World"', [r'"XXHello\u00a0WorldXX"', r'"hello\u00a0world"', r'"HELLO\u00a0WORLD"']), + # Raw strings: escapes are literal, so case changes are NOT equivalent + (r'r"\u00a0"', [r'r"XX\u00a0XX"', r'r"\u00A0"']), ("lambda: 0", ["lambda: 1", "lambda: None"]), ("1 in (1, 2)", ["2 in (1, 2)", "1 not in (1, 2)", "1 in (2, 2)", "1 in (1, 3)"]), ("1+1", ["2+1", "1 - 1", "1+2"]),