-
Notifications
You must be signed in to change notification settings - Fork 208
feat(mutators): Add default parameter mutator. #2863
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
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,152 @@ | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using Moq; | ||
using Shouldly; | ||
using Stryker.Core.Mutants; | ||
using Stryker.Core.Mutators; | ||
using Stryker.Core.Options; | ||
using Xunit; | ||
|
||
namespace Stryker.Core.UnitTest.Mutators; | ||
|
||
public class DefaultParameterMutatorTests | ||
{ | ||
[Fact] | ||
public void ShouldBeMutationLevelStandard() | ||
{ | ||
var options = new StrykerOptions(); | ||
var orchestratorMock = new Mock<ICsharpMutantOrchestrator>(); | ||
var target = new DefaultParameterMutator(orchestratorMock.Object, options); | ||
target.MutationLevel.ShouldBe(MutationLevel.Standard); | ||
} | ||
|
||
[Fact] | ||
public void ShouldMutateDefaultParameter() | ||
{ | ||
var source = @" | ||
using System; | ||
|
||
class Program | ||
{ | ||
void Method() | ||
{ | ||
DefaultParameterMethod(); | ||
} | ||
|
||
void DefaultParameterMethod(string parameter = ""default"") | ||
{ | ||
Console.WriteLine(parameter); | ||
} | ||
}""; | ||
"; | ||
var syntaxTree = CSharpSyntaxTree.ParseText(source); | ||
GetMutations(syntaxTree).ShouldHaveSingleItem(); | ||
} | ||
|
||
[Fact] | ||
public void ShouldNotMutateExplicitlyUsedDefaultParameter() | ||
{ | ||
var source = @" | ||
using System; | ||
|
||
class Program | ||
{ | ||
void Method() | ||
{ | ||
DefaultParameterMethod(""hello world""); | ||
} | ||
|
||
void DefaultParameterMethod(string parameter = ""default"") | ||
{ | ||
Console.WriteLine(parameter); | ||
} | ||
}""; | ||
"; | ||
var syntaxTree = CSharpSyntaxTree.ParseText(source); | ||
GetMutations(syntaxTree).ShouldBeEmpty(); | ||
} | ||
|
||
[Fact] | ||
public void ShouldNotMutateExplicitlyUsedNamedDefaultParameter() | ||
{ | ||
var source = @" | ||
using System; | ||
|
||
class Program | ||
{ | ||
void Method() | ||
{ | ||
DefaultParameterMethod(parameter2: ""hello world""); | ||
} | ||
|
||
void DefaultParameterMethod(string parameter1 = ""default1"", string parameter2 = ""default2"", string parameter3 = ""default3"") | ||
{ | ||
Console.WriteLine(parameter); | ||
} | ||
}""; | ||
"; | ||
var syntaxTree = CSharpSyntaxTree.ParseText(source); | ||
var mutations = GetMutations(syntaxTree); | ||
|
||
mutations.Count().ShouldBe(2); | ||
mutations.ShouldNotContain(x => x.DisplayName.Contains("parameter2")); | ||
} | ||
|
||
[Fact] | ||
public void ShouldMutateImplicitlyUsedDefaultParameter() | ||
{ | ||
var source = @" | ||
using System; | ||
|
||
class Program | ||
{ | ||
void Method() | ||
{ | ||
DefaultParameterMethod(""Hello world""); | ||
} | ||
|
||
void DefaultParameterMethod(string parameter1 = ""default"", string parameter2 = ""default2"", string parameter3 = ""default3"") | ||
{ | ||
Console.WriteLine(parameter); | ||
} | ||
}""; | ||
"; | ||
var syntaxTree = CSharpSyntaxTree.ParseText(source); | ||
|
||
var mutations = GetMutations(syntaxTree); | ||
|
||
mutations.Count().ShouldBe(2); | ||
mutations.ShouldNotContain(x => x.DisplayName.Contains("parameter1")); | ||
} | ||
|
||
private IEnumerable<Mutation> GetMutations(SyntaxTree tree) | ||
{ | ||
var invocations = tree | ||
.GetRoot() | ||
.DescendantNodes() | ||
.OfType<InvocationExpressionSyntax>(); | ||
var options = new StrykerOptions() | ||
{ | ||
MutationLevel = MutationLevel.Complete | ||
}; | ||
var orchestratorMock = new Mock<ICsharpMutantOrchestrator>(); | ||
orchestratorMock.SetupGet(m => m.Mutators).Returns(new List<IMutator> { new StringMutator() }); | ||
|
||
var target = new DefaultParameterMutator(orchestratorMock.Object, options); | ||
|
||
var semanticModel = GetSemanticModel(tree); | ||
|
||
return invocations.SelectMany(invocation => target.ApplyMutations(invocation, semanticModel)); | ||
} | ||
|
||
private static SemanticModel GetSemanticModel(SyntaxTree tree) | ||
{ | ||
var compilation = CSharpCompilation.Create("Test") | ||
.AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)) | ||
.AddSyntaxTrees(tree); | ||
return compilation.GetSemanticModel(tree); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
using System.Collections.Generic; | ||
using Microsoft.CodeAnalysis; | ||
using Stryker.Core.Mutators; | ||
|
||
namespace Stryker.Core.Mutants; | ||
|
||
public interface ICsharpMutantOrchestrator | ||
{ | ||
IEnumerable<IMutator> Mutators { get; } | ||
MutantPlacer Placer { get; } | ||
bool MustInjectCoverageLogic { get; } | ||
ICollection<Mutant> Mutants { get; set; } | ||
int MutantCount { get; set; } | ||
|
||
/// <summary> | ||
/// Recursively mutates a single SyntaxNode | ||
/// </summary> | ||
/// <param name="input">The current root node</param> | ||
/// <returns>Mutated node</returns> | ||
SyntaxNode Mutate(SyntaxNode input, SemanticModel semanticModel); | ||
|
||
/// <summary> | ||
/// Gets the stored mutants and resets the mutant list to an empty collection | ||
/// </summary> | ||
IReadOnlyCollection<Mutant> GetLatestMutantBatch(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using Stryker.Core.Mutants; | ||
using Stryker.Core.Options; | ||
|
||
namespace Stryker.Core.Mutators; | ||
|
||
public class DefaultParameterMutator : MutatorBase<InvocationExpressionSyntax> | ||
{ | ||
private readonly ICsharpMutantOrchestrator _orchestrator; | ||
private readonly StrykerOptions _options; | ||
|
||
public DefaultParameterMutator(ICsharpMutantOrchestrator orchestrator, StrykerOptions options) | ||
{ | ||
_orchestrator = orchestrator; | ||
_options = options; | ||
} | ||
|
||
public override IEnumerable<Mutation> ApplyMutations(InvocationExpressionSyntax node, SemanticModel semanticModel) | ||
{ | ||
var methodSymbol = (IMethodSymbol)semanticModel.GetSymbolInfo(node).Symbol; | ||
if (methodSymbol is null) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should check whether the symbol is internal or external. We might not want to mutate optional parameters of external APIs like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure how we would be able to detect that |
||
{ | ||
yield break; | ||
} | ||
var parameterSymbols = methodSymbol.Parameters; | ||
|
||
var parametersWithDefaultValues = parameterSymbols.Where(p => p.HasExplicitDefaultValue); | ||
|
||
var overridenDefaultParameters = new List<IParameterSymbol>(); | ||
foreach (var argument in node.ArgumentList.Arguments) | ||
{ | ||
if (argument.NameColon is not null) | ||
{ | ||
overridenDefaultParameters.AddRange(parametersWithDefaultValues.Where(p => p.Name == argument.NameColon.Name.Identifier.ValueText)); | ||
} | ||
else | ||
{ | ||
overridenDefaultParameters.AddRange(parametersWithDefaultValues.Where(p => p.Ordinal == node.ArgumentList.Arguments.IndexOf(argument))); | ||
} | ||
} | ||
|
||
var unoverridenDefaultParameters = parametersWithDefaultValues.Except(overridenDefaultParameters); | ||
|
||
foreach (var parameter in unoverridenDefaultParameters) | ||
{ | ||
var parameterName = parameter.Name; | ||
var argumentNameColon = SyntaxFactory.NameColon(parameterName); | ||
var parameterSyntaxNode = (ParameterSyntax)parameter.DeclaringSyntaxReferences[0].GetSyntax(); | ||
|
||
var parameterDefaultExpressionNode = parameterSyntaxNode.Default!.Value; | ||
var mutatedDefaultValues = MutateDefaultValueNode(parameterDefaultExpressionNode, semanticModel); | ||
|
||
foreach (var mutatedDefaultValue in mutatedDefaultValues) | ||
{ | ||
var namedArgument = SyntaxFactory.Argument(nameColon: argumentNameColon, expression: mutatedDefaultValue, refKindKeyword: default); | ||
|
||
var arguments = node.ArgumentList.Arguments.Add(namedArgument); | ||
var newArgumentList = SyntaxFactory.ArgumentList(arguments); | ||
|
||
yield return new Mutation | ||
{ | ||
OriginalNode = node, | ||
ReplacementNode = node.WithArgumentList(newArgumentList), | ||
DisplayName = $"Default parameter mutation: {parameterName}", | ||
Type = Mutator.DefaultParameter | ||
}; | ||
} | ||
} | ||
} | ||
|
||
public override MutationLevel MutationLevel => MutationLevel.Standard; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. First of all, I would place this at the top of the class, not here. Like in our other mutator classes. And I think we should debate whether this should be a level standard mutation. This could add a lot of mutations. |
||
|
||
private IEnumerable<ExpressionSyntax> MutateDefaultValueNode(SyntaxNode defaultValueNode, SemanticModel semanticModel) | ||
{ | ||
List<ExpressionSyntax> nodeMutations = new(); | ||
foreach (var mutator in _orchestrator.Mutators) | ||
{ | ||
if (mutator is not DefaultParameterMutator) | ||
{ | ||
var mutations = mutator.Mutate(defaultValueNode, semanticModel, _options); | ||
foreach (var mutation in mutations) | ||
{ | ||
nodeMutations.Add((ExpressionSyntax)mutation.ReplacementNode); | ||
} | ||
} | ||
} | ||
|
||
return nodeMutations; | ||
} | ||
} |
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.
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.
The original class is also called
CsharpMutantOrchestrator
..Do you want me to keep it as is, or do you want me to rename both the interface and the class to
CSharp...
?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.
Would be best to fix the casing