diff --git a/mutmut/node_mutation.py b/mutmut/node_mutation.py index 806c42a8..d498c1b5 100644 --- a/mutmut/node_mutation.py +++ b/mutmut/node_mutation.py @@ -96,7 +96,7 @@ def operator_arg_removal( yield node.with_changes(args=[*node.args[:i], *node.args[i + 1 :]]) -supported_str_methods_swap = [ +supported_symmetric_str_methods_swap = [ ("lower", "upper"), ("upper", "lower"), ("lstrip", "rstrip"), @@ -107,24 +107,42 @@ def operator_arg_removal( ("rjust", "ljust"), ("index", "rindex"), ("rindex", "index"), - ("split", "rsplit"), - ("rsplit", "split"), ("removeprefix", "removesuffix"), ("removesuffix", "removeprefix"), ("partition", "rpartition"), ("rpartition", "partition") - ] +] + +supported_unsymmetrical_str_methods_swap = [ + ("split", "rsplit"), + ("rsplit", "split") +] -def operator_string_methods_swap( +def operator_symmetric_string_methods_swap( node: cst.Call ) -> Iterable[cst.Call]: """try to swap string method to opposite e.g. a.lower() -> a.upper()""" - for old_call, new_call in supported_str_methods_swap: + for old_call, new_call in supported_symmetric_str_methods_swap: if m.matches(node.func, m.Attribute(value=m.DoNotCare(), attr=m.Name(value=old_call))): func_name = cst.ensure_type(node.func, cst.Attribute).attr yield node.with_deep_changes(func_name, value=new_call) +def operator_unsymmetrical_string_methods_swap( + node: cst.Call +) -> Iterable[cst.Call]: + """Try to handle specific mutations of string, which useful only in specific args combination.""" + for old_call, new_call in supported_unsymmetrical_str_methods_swap: + if m.matches(node.func, m.Attribute(attr=m.Name(value=old_call))): + if old_call in {"split", "rsplit"}: + # The logic of this "if" operator described here: + # https://github.com/boxed/mutmut/pull/394#issuecomment-2977890188 + key_args: set[str] = {a.keyword.value for a in node.args if a.keyword} # sep or maxsplit or nothing + if len(node.args) == 2 or "maxsplit" in key_args: + func_name = cst.ensure_type(node.func, cst.Attribute).attr + yield node.with_deep_changes(func_name, value=new_call) + + def operator_remove_unary_ops( node: cst.UnaryOperation @@ -238,7 +256,8 @@ def operator_match(node: cst.Match) -> Iterable[cst.CSTNode]: (cst.UnaryOperation, operator_remove_unary_ops), (cst.Call, operator_dict_arguments), (cst.Call, operator_arg_removal), - (cst.Call, operator_string_methods_swap), + (cst.Call, operator_symmetric_string_methods_swap), + (cst.Call, operator_unsymmetrical_string_methods_swap), (cst.Lambda, operator_lambda), (cst.CSTNode, operator_keywords), (cst.CSTNode, operator_swap_op), diff --git a/tests/test_mutation.py b/tests/test_mutation.py index 1cb754c3..3295e48f 100644 --- a/tests/test_mutation.py +++ b/tests/test_mutation.py @@ -62,8 +62,47 @@ def mutated_module(source: str) -> str: ]), ('a.index("+")', ['a.rindex("+")', 'a.index("XX+XX")', 'a.index(None)']), ('a.rindex("+")', ['a.index("+")', 'a.rindex("XX+XX")', 'a.rindex(None)']), - ('a.split()', 'a.rsplit()'), - ('a.rsplit()', 'a.split()'), + ('a.split()', []), + ('a.rsplit()', []), + ('a.split(" ")', ['a.split("XX XX")', 'a.split(None)']), + ('a.rsplit(" ")', ['a.rsplit("XX XX")', 'a.rsplit(None)']), + ('a.split(sep="")', ['a.split(sep="XXXX")', 'a.split(sep=None)']), + ('a.rsplit(sep="")', ['a.rsplit(sep="XXXX")', 'a.rsplit(sep=None)']), + ('a.split(maxsplit=-1)', [ + 'a.rsplit(maxsplit=-1)', 'a.split(maxsplit=+1)', 'a.split(maxsplit=-2)', 'a.split(maxsplit=None)' + ]), + ('a.rsplit(maxsplit=-1)', [ + 'a.split(maxsplit=-1)', 'a.rsplit(maxsplit=+1)', 'a.rsplit(maxsplit=-2)', 'a.rsplit(maxsplit=None)' + ]), + ('a.split(" ", maxsplit=-1)', [ + 'a.split(" ", )', 'a.split(" ", maxsplit=+1)', 'a.split(" ", maxsplit=-2)', + 'a.split(" ", maxsplit=None)', 'a.split("XX XX", maxsplit=-1)', 'a.split(None, maxsplit=-1)', + 'a.split(maxsplit=-1)', 'a.rsplit(" ", maxsplit=-1)' + ]), + ('a.rsplit(" ", maxsplit=-1)', [ + 'a.rsplit(" ", )', 'a.rsplit(" ", maxsplit=+1)', 'a.rsplit(" ", maxsplit=-2)', + 'a.rsplit(" ", maxsplit=None)', 'a.rsplit("XX XX", maxsplit=-1)', 'a.rsplit(None, maxsplit=-1)', + 'a.rsplit(maxsplit=-1)', 'a.split(" ", maxsplit=-1)' + ]), + ('a.split(maxsplit=1)', ['a.split(maxsplit=2)', 'a.split(maxsplit=None)', 'a.rsplit(maxsplit=1)']), + ('a.rsplit(maxsplit=1)', ['a.rsplit(maxsplit=2)', 'a.rsplit(maxsplit=None)', 'a.split(maxsplit=1)']), + ('a.split(" ", 1)', [ + 'a.rsplit(" ", 1)', 'a.split(" ", )', 'a.split(" ", 2)', 'a.split(" ", None)', + 'a.split("XX XX", 1)', 'a.split(1)', 'a.split(None, 1)' + ]), + ('a.rsplit(" ", 1)', [ + 'a.rsplit(" ", )', 'a.rsplit(" ", 2)', 'a.rsplit(" ", None)', 'a.rsplit("XX XX", 1)', + 'a.rsplit(1)', 'a.rsplit(None, 1)', 'a.split(" ", 1)' + ]), + ('a.split(" ", maxsplit=1)', [ + 'a.rsplit(" ", maxsplit=1)', 'a.split(" ", )', 'a.split(" ", maxsplit=2)', 'a.split(" ", maxsplit=None)', + 'a.split("XX XX", maxsplit=1)', 'a.split(None, maxsplit=1)', 'a.split(maxsplit=1)' + ]), + ('a.rsplit(" ", maxsplit=1)', [ + 'a.rsplit(" ", )', 'a.rsplit(" ", maxsplit=2)', 'a.rsplit(" ", maxsplit=None)', + 'a.rsplit("XX XX", maxsplit=1)', 'a.rsplit(None, maxsplit=1)', 'a.rsplit(maxsplit=1)', + 'a.split(" ", maxsplit=1)' + ]), ('a.removeprefix("+")', ['a.removesuffix("+")', 'a.removeprefix("XX+XX")', 'a.removeprefix(None)']), ('a.removesuffix("+")', ['a.removeprefix("+")', 'a.removesuffix("XX+XX")', 'a.removesuffix(None)']), ('a.partition("++")', ['a.rpartition("++")', 'a.partition("XX++XX")', 'a.partition(None)']),