Skip to content
Closed
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
4 changes: 2 additions & 2 deletions src/Stryker.Core/Stryker.Core.UnitTest/AssertExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ public static void ShouldBeSemantically(this SyntaxTree actual, SyntaxTree expec
{
var diff = ScanDiff(actual.GetRoot(), expected.GetRoot());

Console.WriteLine(string.Join(Environment.NewLine, diff));
Console.WriteLine();

throw new ShouldAssertException("The actual syntax tree is not equivalent to the expected syntax tree. See the differences above.");
throw new ShouldAssertException("The actual syntax tree is not equivalent to the expected syntax tree. Differences:"+string.Join(Environment.NewLine, diff));
Comment thread
dupdob marked this conversation as resolved.
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System.Linq;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Shouldly;
using Stryker.Core.Instrumentation;

namespace Stryker.Core.UnitTest.Instrumentation;

[TestClass]
public class RedirectMethodEngineShould
{
[TestMethod]
public void InjectSimpleMutatedMethod()
{
const string OriginalClass = """
class Test
{
public void Basic(int x)
{
x++;
}
}
""";
const string MutatedMethod = @"public void Basic(int x) {x--;}";
var parsedClass = SyntaxFactory.ParseSyntaxTree(OriginalClass).GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>().Single();
var parsedMethod = (MethodDeclarationSyntax) SyntaxFactory.ParseMemberDeclaration(MutatedMethod);
var originalMethod = parsedClass.Members.OfType<MethodDeclarationSyntax>().Single();

var engine = new RedirectMethodEngine();

var injected = engine.InjectRedirect(parsedClass, SyntaxFactory.ParseExpression("ActiveMutation(2)"), originalMethod, parsedMethod);

injected.Members.Count.ShouldBe(3);

var expectedTree = SyntaxFactory.ParseSyntaxTree("""
class Test
{
public void Basic(int x)
{if(ActiveMutation(2)){Basic_1(x);}else{Basic_0(x);}}
public void Basic_0(int x)
{
x++;
}
public void Basic_1(int x) {x--;}
}
""");
var actualTree = SyntaxFactory.ParseSyntaxTree(injected.ToString());
actualTree.ShouldBeSemantically(expectedTree);
}

[TestMethod]
public void RollbackMutatedMethod()
{
const string OriginalClass = """
class Test
{
public void Basic(int x)
{
x++;
}
}
""";
const string MutatedMethod = @"public void Basic(int x) {x--;}";
var parsedClass = SyntaxFactory.ParseSyntaxTree(OriginalClass).GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>().Single();
var parsedMethod = (MethodDeclarationSyntax) SyntaxFactory.ParseMemberDeclaration(MutatedMethod);
var originalMethod = parsedClass.Members.OfType<MethodDeclarationSyntax>().Single();

var engine = new RedirectMethodEngine();
var injected = engine.InjectRedirect(parsedClass, SyntaxFactory.ParseExpression("ActiveMutation(2)"), originalMethod, parsedMethod);

// find the entry point
var mutatedEntry = injected.Members.OfType<MethodDeclarationSyntax>().First( p=> p.Identifier.ToString() == originalMethod.Identifier.ToString());
var rolledBackClass = engine.RemoveInstrumentationFrom(injected ,mutatedEntry);

rolledBackClass.ToString().ShouldBeSemantically(OriginalClass);
}
}
Comment on lines +1 to +78

Copilot AI Jan 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test coverage gap: The tests only cover void methods (public void Basic(int x)). There are no tests for methods with return types (e.g., public int Calculate(int x)). Add test cases that verify the redirect mechanism works correctly for methods that return values, as the current implementation appears to have issues handling return statements.

Copilot uses AI. Check for mistakes.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ protected void ShouldMutateSourceToExpected(string actual, string expected)
{
var syntaxTree = CSharpSyntaxTree.ParseText(actual);
Type[] typeToLoad = [typeof(object), typeof(List<>), typeof(Enumerable), typeof(Nullable<>)];
MetadataReference[] references = typeToLoad.Select( t=> MetadataReference.CreateFromFile(t.Assembly.Location)).ToArray();
var references = typeToLoad.Select( t=> MetadataReference.CreateFromFile(t.Assembly.Location)).Cast<MetadataReference>().ToArray();
Comment thread
dupdob marked this conversation as resolved.
var compilation = CSharpCompilation.Create(null).WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
.WithNullableContextOptions(NullableContextOptions.Enable))
.AddSyntaxTrees(syntaxTree).WithReferences(references);
Expand Down
54 changes: 25 additions & 29 deletions src/Stryker.Core/Stryker.Core.UnitTest/Stryker.Core.UnitTest.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,6 @@
<PropertyGroup>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<Compile Remove="TestResources\ExampleSourceFile.cs" />
<Compile Remove="TestResources\ExampleTestFileA.cs" />
<Compile Remove="TestResources\ExampleTestFileB.cs" />
<Compile Remove="TestResources\ExampleTestFilePreprocessorSymbols.cs" />
</ItemGroup>
<ItemGroup>
<None Remove="TestResources\StrongNameKeyFile.snk" />
</ItemGroup>
<ItemGroup>
<Content Include="TestResources\ExampleTestFilePreprocessorSymbols.cs">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="TestResources\ExampleTestFileB.cs">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="TestResources\ExampleTestFileA.cs">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="TestResources\StrongNameKeyFile.snk">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Content Include="TestResources\ExampleSourceFile.cs">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="LaunchDarkly.EventSource" />
<PackageReference Include="Microsoft.CodeAnalysis.VisualBasic" />
Expand All @@ -55,4 +26,29 @@
<ProjectReference Include="..\..\Stryker.Utilities\Stryker.Utilities.csproj" />
<ProjectReference Include="..\Stryker.Core\Stryker.Core.csproj" />
</ItemGroup>

<ItemGroup>
<None Include="TestResources\ExampleSourceFile.cs">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<Content Include="TestResources\ExampleTestFileA.cs">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<None Include="TestResources\ExampleTestFileB.cs">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<Content Include="TestResources\ExampleTestFilePreprocessorSymbols.cs">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Compile Remove="TestResources\ExampleTestFileA.cs" />
<Content Include="TestResources\ExampleTestFileA.cs" />
<Compile Remove="TestResources\ExampleTestFileB.cs" />
<Content Include="TestResources\ExampleTestFileB.cs" />
<Compile Remove="TestResources\ExampleTestFilePreprocessorSymbols.cs" />
<Content Include="TestResources\ExampleTestFilePreprocessorSymbols.cs" />
<None Remove="TestResources\StrongNameKeyFile.snk" />
<Content Include="TestResources\StrongNameKeyFile.snk">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ private SyntaxTree RemoveCompileErrorMutations(SyntaxTree originalTree, IEnumera
// find the mutated node in the new tree
var nodeToRemove = trackedTree.GetCurrentNode(brokenMutation);
// remove the mutated node using its MutantPlacer remove method and update the tree
trackedTree = trackedTree.ReplaceNode(nodeToRemove, MutantPlacer.RemoveMutant(nodeToRemove));
trackedTree = MutantPlacer.RemoveMutation(nodeToRemove);
}

return trackedTree.SyntaxTree;
Expand Down
32 changes: 32 additions & 0 deletions src/Stryker.Core/Stryker.Core/Helpers/RoslynHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,38 @@ public static bool ContainsNodeThatVerifies(this SyntaxNode node, Func<SyntaxNod
&& (child.Parent is not LocalFunctionStatementSyntax localFunction || localFunction.ExpressionBody != child);
} ).Any(predicate);


/// <summary>
/// Ensure a statement is in a syntax bock.
/// </summary>
/// <param name="statement">the statement to put into a block.</param>
/// <returns>a block containing <paramref name="statement"/>, or <paramref name="statement"/> if it is already a block</returns>
public static BlockSyntax AsBlock(this StatementSyntax statement) => statement as BlockSyntax ?? SyntaxFactory.Block(statement);

/// <summary>
/// Ensure an expression is in a syntax bock.
/// </summary>
/// <param name="expression">the expression to put into a block.</param>
/// <returns>a block containing <paramref name="expression"/></returns>
public static BlockSyntax AsBlock(this ExpressionSyntax expression) =>SyntaxFactory.ExpressionStatement(expression).AsBlock();
Comment thread
dupdob marked this conversation as resolved.

/// <summary>
/// Ensure a <see cref="SyntaxNode"/> is followed by a trailing newline
/// </summary>
/// <typeparam name="T">Type of node, must be a SyntaxNode</typeparam>
/// <param name="node">Node</param>
/// <returns><paramref name="node"/> with a trailing newline</returns>
public static T WithTrailingNewLine<T>(this T node) where T: SyntaxNode
=> node.WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed);


public static ClassDeclarationSyntax RemoveNamedMember(this ClassDeclarationSyntax classNode, string memberName) =>
classNode.RemoveNode(classNode.Members.First( m => m switch
{
MethodDeclarationSyntax method => method.Identifier.ToString() == memberName,
PropertyDeclarationSyntax field => field.Identifier.ToString() == memberName,
_ => false
}), SyntaxRemoveOptions.KeepNoTrivia);
Comment on lines +227 to +233

Copilot AI Jan 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential null reference exception: If no member with the specified name is found, First() will throw an InvalidOperationException. Consider using FirstOrDefault() and handling the null case, or adding a more descriptive exception message that explains which member was not found.

Suggested change
public static ClassDeclarationSyntax RemoveNamedMember(this ClassDeclarationSyntax classNode, string memberName) =>
classNode.RemoveNode(classNode.Members.First( m => m switch
{
MethodDeclarationSyntax method => method.Identifier.ToString() == memberName,
PropertyDeclarationSyntax field => field.Identifier.ToString() == memberName,
_ => false
}), SyntaxRemoveOptions.KeepNoTrivia);
public static ClassDeclarationSyntax RemoveNamedMember(this ClassDeclarationSyntax classNode, string memberName)
{
var memberToRemove = classNode.Members.FirstOrDefault(m => m switch
{
MethodDeclarationSyntax method => method.Identifier.ToString() == memberName,
PropertyDeclarationSyntax field => field.Identifier.ToString() == memberName,
_ => false
});
if (memberToRemove is null)
{
throw new InvalidOperationException($"Member '{memberName}' was not found in class '{classNode.Identifier}'.");
}
return classNode.RemoveNode(memberToRemove, SyntaxRemoveOptions.KeepNoTrivia);
}

Copilot uses AI. Check for mistakes.
/// <summary>
/// Cleaned trivia from a node
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions src/Stryker.Core/Stryker.Core/Instrumentation/BaseEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,10 @@ public SyntaxNode RemoveInstrumentation(SyntaxNode node)
}
throw new InvalidOperationException($"Expected a {typeof(T).Name}, found:\n{node.ToFullString()}.");
}

public virtual SyntaxNode RemoveInstrumentationFrom(SyntaxNode tree, SyntaxNode instrumentation)
{
var restoredNode = RemoveInstrumentation(instrumentation);
return tree.ReplaceNode(instrumentation, restoredNode);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@ public interface IInstrumentCode
/// <returns>returns a node without the instrumentation.</returns>
/// <exception cref="InvalidOperationException">if the node was not instrumented (by this instrumentingEngine)</exception>
SyntaxNode RemoveInstrumentation(SyntaxNode node);

SyntaxNode RemoveInstrumentationFrom(SyntaxNode tree, SyntaxNode instrumentation);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Stryker.Core.Helpers;

namespace Stryker.Core.Instrumentation;

Expand All @@ -20,13 +21,11 @@ internal class IfInstrumentationEngine : BaseEngine<IfStatementSyntax>
/// <remarks>This method works with statement and block.</remarks>
public IfStatementSyntax InjectIf(ExpressionSyntax condition, StatementSyntax originalNode, StatementSyntax mutatedNode)
=> SyntaxFactory.IfStatement(condition,
AsBlock(mutatedNode),
SyntaxFactory.ElseClause(AsBlock(originalNode.WithoutTrivia()))).
mutatedNode.AsBlock(),
SyntaxFactory.ElseClause(originalNode.WithoutTrivia().AsBlock())).
WithTriviaFrom(originalNode).
WithAdditionalAnnotations(Marker);

private static BlockSyntax AsBlock(StatementSyntax code) => code as BlockSyntax ?? SyntaxFactory.Block(code);

/// <summary>
/// Returns the original code.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using System;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Stryker.Core.Helpers;

namespace Stryker.Core.Instrumentation;

Copilot AI Jan 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing XML documentation: This internal class lacks XML documentation comments. For consistency with other internal classes in this namespace (e.g., ConditionalInstrumentationEngine, IfInstrumentationEngine, DefaultInitializationEngine), consider adding a summary comment that describes the purpose of this class: injecting method-level mutations via method redirection.

Suggested change
/// <summary>
/// Provides method-level mutation injection by redirecting calls through a conditional wrapper
/// that selects between the original and mutated method implementations.
/// </summary>

Copilot uses AI. Check for mistakes.
internal class RedirectMethodEngine : BaseEngine<MethodDeclarationSyntax>
{
private const string _redirectHints = "RedirectHints";

public ClassDeclarationSyntax InjectRedirect(ClassDeclarationSyntax originalClass,
ExpressionSyntax condition,
MethodDeclarationSyntax originalMethod,
MethodDeclarationSyntax mutatedMethod)
Comment on lines +14 to +17

Copilot AI Jan 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing XML documentation: This public method lacks XML documentation. For consistency with other public methods in similar classes (e.g., InjectIf in IfInstrumentationEngine, PlaceWithConditionalExpression in ConditionalInstrumentationEngine), add XML comments describing the parameters and return value. Document what InjectRedirect does: it creates a method redirect pattern where the original method is renamed, a mutated version is added, and a dispatcher method with the original name routes calls based on a condition.

Copilot uses AI. Check for mistakes.
{
if (!originalClass.Contains(originalMethod))
{
throw new ArgumentException($"Syntax tree does not contains {originalMethod.Identifier}.", nameof(originalMethod));

Copilot AI Jan 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grammatical error in exception message: "does not contains" should be "does not contain"

Suggested change
throw new ArgumentException($"Syntax tree does not contains {originalMethod.Identifier}.", nameof(originalMethod));
throw new ArgumentException($"Syntax tree does not contain {originalMethod.Identifier}.", nameof(originalMethod));

Copilot uses AI. Check for mistakes.
}

// find alternative names
var index = 0;
var newNameForOriginal = FindNewName(originalClass, originalMethod, ref index);
var newNameForMutated = FindNewName(originalClass, originalMethod, ref index);

// generates a redirecting method
// generate calls to the redirected method
var originalCall = GenerateRedirectedInvocation(originalMethod, newNameForOriginal);
var mutatedCall = GenerateRedirectedInvocation(originalMethod, newNameForMutated);

var redirectHints = new SyntaxAnnotation(_redirectHints, $"{originalMethod.Identifier.ToString()},{newNameForOriginal},{newNameForMutated}");
Comment thread
dupdob marked this conversation as resolved.

var redirector = originalMethod
.WithBody(SyntaxFactory.Block(
SyntaxFactory.IfStatement(condition, mutatedCall.AsBlock(),
SyntaxFactory.ElseClause(originalCall.AsBlock())
))).WithExpressionBody(null).WithoutLeadingTrivia();
Comment on lines +36 to +40

Copilot AI Jan 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential logic issue: The generated invocations (originalCall and mutatedCall) are placed as expression statements in the if/else blocks without return statements. This will cause compilation errors for methods with return types. For methods returning a value, the invocations should be wrapped in return statements (e.g., return originalCall; instead of just originalCall;). Check if originalMethod.ReturnType indicates a non-void return type and add return statements accordingly.

Suggested change
var redirector = originalMethod
.WithBody(SyntaxFactory.Block(
SyntaxFactory.IfStatement(condition, mutatedCall.AsBlock(),
SyntaxFactory.ElseClause(originalCall.AsBlock())
))).WithExpressionBody(null).WithoutLeadingTrivia();
var isVoid = originalMethod.ReturnType is PredefinedTypeSyntax predefinedType
&& predefinedType.Keyword.IsKind(SyntaxKind.VoidKeyword);
var originalStatement = isVoid
? (StatementSyntax)SyntaxFactory.ExpressionStatement(originalCall)
: SyntaxFactory.ReturnStatement(originalCall);
var mutatedStatement = isVoid
? (StatementSyntax)SyntaxFactory.ExpressionStatement(mutatedCall)
: SyntaxFactory.ReturnStatement(mutatedCall);
var redirector = originalMethod
.WithBody(SyntaxFactory.Block(
SyntaxFactory.IfStatement(
condition,
SyntaxFactory.Block(mutatedStatement),
SyntaxFactory.ElseClause(SyntaxFactory.Block(originalStatement)))))
.WithExpressionBody(null)
.WithoutLeadingTrivia();

Copilot uses AI. Check for mistakes.

// update the class
var resultingClass = originalClass.RemoveNode(originalMethod, SyntaxRemoveOptions.KeepNoTrivia)
?.AddMembers([redirector.WithTrailingNewLine().WithAdditionalAnnotations(redirectHints, Marker),

Copilot AI Jan 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential null reference: RemoveNode can return null if the node is not found. The null-conditional operator ?. is used, but the method returns a non-nullable ClassDeclarationSyntax, which means a null return value would lead to a null reference exception. Either handle the null case explicitly or ensure that RemoveNode cannot return null in this context.

Suggested change
?.AddMembers([redirector.WithTrailingNewLine().WithAdditionalAnnotations(redirectHints, Marker),
.AddMembers([redirector.WithTrailingNewLine().WithAdditionalAnnotations(redirectHints, Marker),

Copilot uses AI. Check for mistakes.
originalMethod.WithIdentifier(SyntaxFactory.Identifier(newNameForOriginal)).WithTrailingNewLine().WithAdditionalAnnotations(redirectHints, Marker),
mutatedMethod.WithIdentifier(SyntaxFactory.Identifier(newNameForMutated)).WithTrailingNewLine().WithAdditionalAnnotations(redirectHints, Marker)]);
return resultingClass;
}

private static InvocationExpressionSyntax GenerateRedirectedInvocation(MethodDeclarationSyntax originalMethod, string redirectedName)
=> SyntaxFactory.InvocationExpression(SyntaxFactory.IdentifierName(redirectedName),
SyntaxFactory.ArgumentList( SyntaxFactory.SeparatedList(
originalMethod.ParameterList.Parameters.Select(p => SyntaxFactory.Argument( SyntaxFactory.IdentifierName(p.Identifier))))));

private static string FindNewName(ClassDeclarationSyntax originalClass, MethodDeclarationSyntax originalMethod, ref int index)
{
string newNameForOriginal;
do
{
newNameForOriginal = $"{originalMethod.Identifier}_{index++}";
}
while (originalClass.Members.Any(m => m is MethodDeclarationSyntax method && method.Identifier.ToFullString() == newNameForOriginal));
return newNameForOriginal;
}

protected override SyntaxNode Revert(MethodDeclarationSyntax node) => throw new NotSupportedException("Cannot revert node in place.");

public override SyntaxNode RemoveInstrumentationFrom(SyntaxNode tree, SyntaxNode instrumentation)
{
var annotation = instrumentation.GetAnnotations(_redirectHints).FirstOrDefault()?.Data;
if (string.IsNullOrEmpty(annotation))
{
throw new InvalidOperationException($"Unable to find details to rollback this instrumentation: '{instrumentation}'");
}

var method = (MethodDeclarationSyntax) instrumentation;
var names = annotation.Split(',').ToList();


Comment thread
dupdob marked this conversation as resolved.
var parentClass = (ClassDeclarationSyntax) method.Parent;
var renamedMethod = (MethodDeclarationSyntax) parentClass.Members.
First( m=> m is MethodDeclarationSyntax meth && meth.Identifier.Text == names[1]);
Comment on lines +81 to +82

Copilot AI Jan 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential InvalidOperationException: First() will throw an exception if no method with the specified identifier is found in the members. Consider using FirstOrDefault() and adding a more descriptive error message if the method is not found.

Suggested change
var renamedMethod = (MethodDeclarationSyntax) parentClass.Members.
First( m=> m is MethodDeclarationSyntax meth && meth.Identifier.Text == names[1]);
var renamedMethod = parentClass.Members
.OfType<MethodDeclarationSyntax>()
.FirstOrDefault(meth => meth.Identifier.Text == names[1]);
if (renamedMethod is null)
{
throw new InvalidOperationException($"Unable to find method '{names[1]}' to rollback this instrumentation in class '{parentClass.Identifier.Text}'.");
}

Copilot uses AI. Check for mistakes.
parentClass = parentClass.TrackNodes(renamedMethod);
// we need to remove redirection method and replacement method and restore the name of the original method
parentClass = parentClass.RemoveNamedMember(names[2]).RemoveNamedMember(names[0]);
var oldNode = parentClass.GetCurrentNode(renamedMethod);
parentClass = parentClass.ReplaceNode(oldNode, renamedMethod.WithIdentifier(SyntaxFactory.Identifier(names[0])));
return parentClass;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ public SyntaxNode RemoveInstrumentation(SyntaxNode node)
return SwitchToThisBodies(typedNode, null, expression).WithoutAnnotations(Marker);
}

public SyntaxNode RemoveInstrumentationFrom(SyntaxNode tree, SyntaxNode instrumentation)
{
var restoredNode = RemoveInstrumentation(instrumentation);
return tree.ReplaceNode(instrumentation, restoredNode);
}

/// <inheritdoc/>
protected override T InjectMutations(T sourceNode, T targetNode, SemanticModel semanticModel, MutationContext context)
{
Expand Down
14 changes: 14 additions & 0 deletions src/Stryker.Core/Stryker.Core/Mutants/MutantPlacer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,20 @@ public static SyntaxNode RemoveMutant(SyntaxNode nodeToRemove)
throw new InvalidOperationException($"Unable to find an engine to remove injection from this node: '{nodeToRemove}'");
}

public static SyntaxNode RemoveMutation(SyntaxNode nodeToRemove)
{
var annotatedNode = nodeToRemove.GetAnnotatedNodes(Injector).FirstOrDefault();
if (annotatedNode != null)
{
var id = annotatedNode.GetAnnotations(Injector).First().Data;
if (!string.IsNullOrEmpty(id))
{
return instrumentEngines[id].engine.RemoveInstrumentationFrom(nodeToRemove.SyntaxTree.GetRoot(), annotatedNode);
}
}
throw new InvalidOperationException($"Unable to find an engine to remove injection from this node: '{nodeToRemove}'");
}
Comment on lines +155 to +167

Copilot AI Jan 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential logic issue: The method always returns the entire syntax tree root from RemoveInstrumentationFrom, but the calling code in CSharpRollbackProcess.cs is tracking nodes and expects to update them incrementally. The old RemoveMutant method would return a node that could be tracked and replaced, but this new method returns the entire tree root, which may break the node tracking logic. Consider whether the assignment trackedTree = MutantPlacer.RemoveMutation(nodeToRemove); should instead be using a pattern that preserves the tracked tree structure.

Copilot uses AI. Check for mistakes.

/// <summary>
/// Returns true if the node contains a mutation requiring all child mutations to be removed when it has to be removed
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Stryker.Core/Stryker.Core/Mutants/MutationStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

namespace Stryker.Core.Mutants;


/// <summary>
/// This enum is used to track the syntax 'level' of mutations that are injected in the code.
/// </summary>
Expand Down Expand Up @@ -143,6 +142,7 @@ public bool StoreMutationsAtDesiredLevel(IEnumerable<Mutant> store, MutationCont
controller.StoreMutations(store);
return true;
}

Logger.LogDebug("There is no structure to control {MutationsCount} mutations. They are dropped.", store.Count());
foreach (var mutant in store)
{
Expand Down
Loading
Loading