Skip to content

Add class level mutation control (for method only)#3131

Draft
dupdob wants to merge 6 commits intomasterfrom
memberMutantSupport
Draft

Add class level mutation control (for method only)#3131
dupdob wants to merge 6 commits intomasterfrom
memberMutantSupport

Conversation

@dupdob
Copy link
Copy Markdown
Member

@dupdob dupdob commented Dec 2, 2024

This is an extension of the mutation logic design to support mutations for whole methods.

@dupdob dupdob force-pushed the memberMutantSupport branch from 70e3fbb to ccb073c Compare December 2, 2024 10:41
@dupdob dupdob force-pushed the memberMutantSupport branch from ccb073c to b78fc59 Compare December 3, 2024 13:31
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Dec 3, 2024

Quality Gate Failed Quality Gate failed

Failed conditions
1 New Minor Issues (required ≤ 0)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Copilot AI review requested due to automatic review settings January 29, 2026 15:18
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR extends Stryker.NET's mutation logic to support method-level mutations. Instead of mutating individual expressions within a method, it introduces a new mechanism where entire methods can be mutated by creating renamed copies of the original and mutated versions, with a redirector method that dispatches to the appropriate version based on mutation activation conditions.

Changes:

  • Introduces RedirectMethodEngine for method-level mutation instrumentation via method redirection
  • Adds helper methods to RoslynHelper for code generation support (AsBlock, WithTrailingNewLine, RemoveNamedMember)
  • Updates instrumentation interfaces and base classes to support removal from the entire syntax tree
  • Modifies rollback process to use the new RemoveMutation method for class-level removals

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 17 comments.

Show a summary per file
File Description
src/Stryker.Core/Stryker.Core/Instrumentation/RedirectMethodEngine.cs New engine that implements method-level mutation via method redirection pattern
src/Stryker.Core/Stryker.Core/Instrumentation/IInstrumentCode.cs Adds RemoveInstrumentationFrom method to interface for tree-level removal
src/Stryker.Core/Stryker.Core/Instrumentation/BaseEngine.cs Implements default RemoveInstrumentationFrom method
src/Stryker.Core/Stryker.Core/Instrumentation/IfInstrumentationEngine.cs Refactors to use new AsBlock helper method
src/Stryker.Core/Stryker.Core/Mutants/MutantPlacer.cs Adds new RemoveMutation method for removing mutations from entire tree
src/Stryker.Core/Stryker.Core/Mutants/CsharpNodeOrchestrators/BaseFunctionOrchestrator.cs Implements RemoveInstrumentationFrom for function orchestrators
src/Stryker.Core/Stryker.Core/Mutants/MutationStore.cs Formatting cleanup (empty line removal/addition)
src/Stryker.Core/Stryker.Core/Helpers/RoslynHelper.cs Adds helper extension methods for syntax manipulation
src/Stryker.Core/Stryker.Core/Compiling/CSharpRollbackProcess.cs Updates to use new RemoveMutation method
src/Stryker.TestRunner.VsTest.UnitTest/VsTestMockingHelper.cs Apparent syntax error in constructor initialization
src/Stryker.Core/Stryker.Core.UnitTest/Instrumentation/RedirectMethodEngineShould.cs New test file for RedirectMethodEngine functionality
src/Stryker.Core/Stryker.Core.UnitTest/AssertExtensions.cs Improves error message formatting in test assertions
src/Stryker.Core/Stryker.Core.UnitTest/Mutants/MutantOrchestratorTestsBase.cs Adds unnecessary cast operation
src/Stryker.TestRunner.VsTest.UnitTest/Stryker.TestRunner.VsTest.UnitTest.csproj Adds compile exclusions for test resources
src/Stryker.Core/Stryker.Core.UnitTest/Stryker.Core.UnitTest.csproj Reorganizes test resource file handling with different copy settings

{
if (!originalClass.Contains(originalMethod))
{
throw new ArgumentException($"Syntax tree does not contains {originalMethod.Identifier}.", nameof(originalMethod));
Copy link

Copilot AI Jan 29, 2026

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.
Comment on lines +155 to +167
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}'");
}
Copy link

Copilot AI Jan 29, 2026

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.
Comment on lines +14 to +17
public ClassDeclarationSyntax InjectRedirect(ClassDeclarationSyntax originalClass,
ExpressionSyntax condition,
MethodDeclarationSyntax originalMethod,
MethodDeclarationSyntax mutatedMethod)
Copy link

Copilot AI Jan 29, 2026

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.
Comment on lines +227 to +233
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);
Copy link

Copilot AI Jan 29, 2026

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.
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
1 New Minor Issues (required ≤ 0)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants