-
Notifications
You must be signed in to change notification settings - Fork 235
Add class level mutation control (for method only) #3131
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0cd64fa
b78fc59
a0f7338
07d2ffe
4ffb1be
5944817
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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(); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
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
|
||||||||||||||||||||||||||||||||||||||||||||||||
| 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); | |
| } |
| 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; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | |
| /// Provides method-level mutation injection by redirecting calls through a conditional wrapper | |
| /// that selects between the original and mutated method implementations. | |
| /// </summary> |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
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
AI
Jan 29, 2026
There was a problem hiding this comment.
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"
| 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
AI
Jan 29, 2026
There was a problem hiding this comment.
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.
| 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
AI
Jan 29, 2026
There was a problem hiding this comment.
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.
| ?.AddMembers([redirector.WithTrailingNewLine().WithAdditionalAnnotations(redirectHints, Marker), | |
| .AddMembers([redirector.WithTrailingNewLine().WithAdditionalAnnotations(redirectHints, Marker), |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
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.
| 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}'."); | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
|
||
|
|
||
| /// <summary> | ||
| /// Returns true if the node contains a mutation requiring all child mutations to be removed when it has to be removed | ||
| /// </summary> | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.