From a32280e95c033e11b697b613a3e1a3433789293e Mon Sep 17 00:00:00 2001 From: CHB_0r1s Date: Sat, 14 Jun 2025 04:37:06 +0300 Subject: [PATCH 1/4] fix(str_swap): Fix simmetric mutations on split rsplit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Лузин Борис Евгеньевич --- mutmut/node_mutation.py | 66 +++++++++++++++++++++++++++++++++++++++-- tests/test_mutation.py | 39 ++++++++++++++++++++++-- 2 files changed, 100 insertions(+), 5 deletions(-) diff --git a/mutmut/node_mutation.py b/mutmut/node_mutation.py index 806c42a8..9ee42176 100644 --- a/mutmut/node_mutation.py +++ b/mutmut/node_mutation.py @@ -107,13 +107,16 @@ 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( node: cst.Call @@ -125,6 +128,62 @@ def operator_string_methods_swap( func_name = cst.ensure_type(node.func, cst.Attribute).attr yield node.with_deep_changes(func_name, value=new_call) +def 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(value=m.DoNotCare(), attr=m.Name(value=old_call))): + if old_call in {"split", "rsplit"}: + # split() -> split() # not len(node.args) + # rsplit() -> rsplit() # not len(node.args) + # split(" ") -> split(" ") # len(node.args) == 1 & maxsplit not in args + # rsplit(" ") -> rsplit(" ") # len(node.args) == 1 & maxsplit not in args + # split(sep=" ") -> split(sep=" ") # len(node.args) == 1 & maxsplit not in args + # rsplit(sep=" ") -> rsplit(sep=" ") # len(node.args) == 1 & maxsplit not in args + # split(maxsplit=-1) -> split(maxsplit=-1) # maxsplit=-1 in args + # rsplit(maxsplit=-1) -> rsplit(maxsplit=-1) # maxsplit=-1 in args + # split(" ", maxsplit=-1) -> split(" ", maxsplit=-1) # maxsplit=-1 in args + # rsplit(" ", maxsplit=-1) -> rsplit(" ", maxsplit=-1) # maxsplit=-1 in args + + # split(maxsplit=1) -> rsplit(maxsplit=1) # maxsplit in args and maxsplit != -1 + # rsplit(maxsplit=1) -> split(maxsplit=1) # maxsplit in args and maxsplit != -1 + + # split(" ", 1) -> rsplit(" ", 1) # len(node.args) == 2 & maxsplit not in args + # rsplit(" ", 1) -> split(" ", 1) # len(node.args) == 2 & maxsplit not in args + + # split(" ", maxsplit=1) -> rsplit(" ", maxsplit=1) # maxsplit in args and maxsplit != -1 + # rsplit(" ", maxsplit=1) -> split(" ", maxsplit=1) # maxsplit in args and maxsplit != -1 + key_args: set[str] = {a.keyword.value for a in node.args if a.keyword} + + maxsplit_val = None + for a in node.args: + if a.keyword and a.keyword.value == "maxsplit": + if any([isinstance(child, cst.Minus) for child in a.value.children]): + maxsplit_val = int(a.value.expression.value) * -1 + break + if any([isinstance(child, cst.Plus) for child in a.value.children]): + maxsplit_val = int(a.value.expression.value) + break + maxsplit_val = a.value.evaluated_value + + + if ( + not len(node.args) or + (len(node.args) == 1 and "maxsplit" not in key_args) or + ("maxsplit" in key_args and maxsplit_val == -1) + ): + continue + + if ( + (len(node.args) == 2 and "maxsplit" not in key_args) or + ("maxsplit" in key_args and maxsplit_val != -1) + ): + 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 @@ -239,6 +298,7 @@ def operator_match(node: cst.Match) -> Iterable[cst.CSTNode]: (cst.Call, operator_dict_arguments), (cst.Call, operator_arg_removal), (cst.Call, operator_string_methods_swap), + (cst.Call, 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..95da2c74 100644 --- a/tests/test_mutation.py +++ b/tests/test_mutation.py @@ -62,8 +62,43 @@ 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.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(" ", )', '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(" ", )', '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=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)']), From b7a8c3717f4683ccd64969414db5d0a65dba0ace Mon Sep 17 00:00:00 2001 From: CHB_0r1s Date: Mon, 16 Jun 2025 23:14:50 +0300 Subject: [PATCH 2/4] fix(str_swap): Remove check of maxsplit value --- mutmut/node_mutation.py | 49 ++++------------------------------------- tests/test_mutation.py | 12 ++++++---- 2 files changed, 12 insertions(+), 49 deletions(-) diff --git a/mutmut/node_mutation.py b/mutmut/node_mutation.py index 9ee42176..ecf896f0 100644 --- a/mutmut/node_mutation.py +++ b/mutmut/node_mutation.py @@ -132,54 +132,13 @@ def 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(value=m.DoNotCare(), attr=m.Name(value=old_call))): + if m.matches(node.func, m.Attribute(attr=m.Name(value=old_call))): if old_call in {"split", "rsplit"}: - # split() -> split() # not len(node.args) - # rsplit() -> rsplit() # not len(node.args) - # split(" ") -> split(" ") # len(node.args) == 1 & maxsplit not in args - # rsplit(" ") -> rsplit(" ") # len(node.args) == 1 & maxsplit not in args - # split(sep=" ") -> split(sep=" ") # len(node.args) == 1 & maxsplit not in args - # rsplit(sep=" ") -> rsplit(sep=" ") # len(node.args) == 1 & maxsplit not in args - # split(maxsplit=-1) -> split(maxsplit=-1) # maxsplit=-1 in args - # rsplit(maxsplit=-1) -> rsplit(maxsplit=-1) # maxsplit=-1 in args - # split(" ", maxsplit=-1) -> split(" ", maxsplit=-1) # maxsplit=-1 in args - # rsplit(" ", maxsplit=-1) -> rsplit(" ", maxsplit=-1) # maxsplit=-1 in args - - # split(maxsplit=1) -> rsplit(maxsplit=1) # maxsplit in args and maxsplit != -1 - # rsplit(maxsplit=1) -> split(maxsplit=1) # maxsplit in args and maxsplit != -1 - - # split(" ", 1) -> rsplit(" ", 1) # len(node.args) == 2 & maxsplit not in args - # rsplit(" ", 1) -> split(" ", 1) # len(node.args) == 2 & maxsplit not in args - - # split(" ", maxsplit=1) -> rsplit(" ", maxsplit=1) # maxsplit in args and maxsplit != -1 - # rsplit(" ", maxsplit=1) -> split(" ", maxsplit=1) # maxsplit in args and maxsplit != -1 + # 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} - - maxsplit_val = None - for a in node.args: - if a.keyword and a.keyword.value == "maxsplit": - if any([isinstance(child, cst.Minus) for child in a.value.children]): - maxsplit_val = int(a.value.expression.value) * -1 - break - if any([isinstance(child, cst.Plus) for child in a.value.children]): - maxsplit_val = int(a.value.expression.value) - break - maxsplit_val = a.value.evaluated_value - - - if ( - not len(node.args) or - (len(node.args) == 1 and "maxsplit" not in key_args) or - ("maxsplit" in key_args and maxsplit_val == -1) - ): - continue - - if ( - (len(node.args) == 2 and "maxsplit" not in key_args) or - ("maxsplit" in key_args and maxsplit_val != -1) - ): + 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) diff --git a/tests/test_mutation.py b/tests/test_mutation.py index 95da2c74..3295e48f 100644 --- a/tests/test_mutation.py +++ b/tests/test_mutation.py @@ -68,17 +68,21 @@ def mutated_module(source: str) -> str: ('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.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.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.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.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)']), From 43e3bfb0a4e252ebf5051665227530ab6c60f1fc Mon Sep 17 00:00:00 2001 From: CHB_0r1s Date: Mon, 16 Jun 2025 23:37:00 +0300 Subject: [PATCH 3/4] fix(str_swap): Add comment --- mutmut/node_mutation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mutmut/node_mutation.py b/mutmut/node_mutation.py index ecf896f0..490880e0 100644 --- a/mutmut/node_mutation.py +++ b/mutmut/node_mutation.py @@ -137,7 +137,7 @@ def unsymmetrical_string_methods_swap( 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} + 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) From 0f34019cb1216a3dd74adc672dae643492c899c2 Mon Sep 17 00:00:00 2001 From: CHB_0r1s Date: Mon, 16 Jun 2025 23:58:31 +0300 Subject: [PATCH 4/4] fix(str_swap): fix naming Signed-off-by: CHB-0r1s --- mutmut/node_mutation.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mutmut/node_mutation.py b/mutmut/node_mutation.py index 490880e0..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"), @@ -118,17 +118,17 @@ def operator_arg_removal( ("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 unsymmetrical_string_methods_swap( +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.""" @@ -256,8 +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, unsymmetrical_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),