@@ -241,7 +241,7 @@ def function_trampoline_arrangement(function: cst.FunctionDef, mutants: Iterable
241241 mutant_name = f'{ mangled_name } _{ i + 1 } '
242242 mutant_names .append (mutant_name )
243243 mutated_method = function .with_changes (name = cst .Name (mutant_name ))
244- mutated_method = mutated_method . deep_replace (mutant .original_node , mutant .mutated_node )
244+ mutated_method = deep_replace (mutated_method , mutant .original_node , mutant .mutated_node )
245245 nodes .append (mutated_method ) # type: ignore
246246
247247 # trampoline that forwards the calls
@@ -298,4 +298,26 @@ def pragma_no_mutate_lines(source: str) -> set[int]:
298298 i + 1
299299 for i , line in enumerate (source .split ('\n ' ))
300300 if '# pragma:' in line and 'no mutate' in line .partition ('# pragma:' )[- 1 ]
301- }
301+ }
302+
303+ def deep_replace (tree : cst .CSTNode , old_node : cst .CSTNode , new_node : cst .CSTNode ) -> cst .CSTNode :
304+ """Like the CSTNode.deep_replace method, except that we only replace up to one occurence of old_node."""
305+ return tree .visit (ChildReplacementTransformer (old_node , new_node )) # type: ignore
306+
307+ class ChildReplacementTransformer (cst .CSTTransformer ):
308+ def __init__ (self , old_node : cst .CSTNode , new_node : cst .CSTNode ):
309+ self .old_node = old_node
310+ self .new_node = new_node
311+ self .replaced_node = False
312+
313+ def on_visit (self , node : cst .CSTNode ) -> bool :
314+ # If the node is one we are about to replace, we shouldn't
315+ # recurse down it, that would be a waste of time.
316+ # Also, we stop recursion when we already replaced the node.
317+ return not (self .replaced_node or node is self .old_node )
318+
319+ def on_leave (self , original_node : cst .CSTNode , updated_node : cst .CSTNode ) -> cst .CSTNode :
320+ if original_node is self .old_node :
321+ self .replaced_node = True
322+ return self .new_node
323+ return updated_node
0 commit comments