diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/DefaultParameterMutatorTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/DefaultParameterMutatorTests.cs new file mode 100644 index 0000000000..d190a526a8 --- /dev/null +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/DefaultParameterMutatorTests.cs @@ -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(); + 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 GetMutations(SyntaxTree tree) + { + var invocations = tree + .GetRoot() + .DescendantNodes() + .OfType(); + var options = new StrykerOptions() + { + MutationLevel = MutationLevel.Complete + }; + var orchestratorMock = new Mock(); + orchestratorMock.SetupGet(m => m.Mutators).Returns(new List { 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); + } +} diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Options/Inputs/IgnoreMutationsInputTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Options/Inputs/IgnoreMutationsInputTests.cs index ca06d5e4e2..a1b9572ff2 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Options/Inputs/IgnoreMutationsInputTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Options/Inputs/IgnoreMutationsInputTests.cs @@ -27,7 +27,7 @@ public void ShouldValidateExcludedMutation() var ex = Should.Throw(() => target.Validate()); - ex.Message.ShouldBe($"Invalid excluded mutation (gibberish). The excluded mutations options are [Statement, Arithmetic, Block, Equality, Boolean, Logical, Assignment, Unary, Update, Checked, Linq, String, Bitwise, Initializer, Regex, NullCoalescing, Math]"); + ex.Message.ShouldBe($"Invalid excluded mutation (gibberish). The excluded mutations options are [Statement, Arithmetic, Block, Equality, Boolean, Logical, Assignment, Unary, Update, Checked, Linq, String, Bitwise, Initializer, Regex, NullCoalescing, Math, DefaultParameter]"); } [Fact] diff --git a/src/Stryker.Core/Stryker.Core/Mutants/CsharpMutantOrchestrator.cs b/src/Stryker.Core/Stryker.Core/Mutants/CsharpMutantOrchestrator.cs index 90490277e2..84b78412ea 100644 --- a/src/Stryker.Core/Stryker.Core/Mutants/CsharpMutantOrchestrator.cs +++ b/src/Stryker.Core/Stryker.Core/Mutants/CsharpMutantOrchestrator.cs @@ -14,8 +14,8 @@ namespace Stryker.Core.Mutants { - /// - public class CsharpMutantOrchestrator : BaseMutantOrchestrator + /// + public class CsharpMutantOrchestrator : BaseMutantOrchestrator, ICsharpMutantOrchestrator { private readonly TypeBasedStrategy _specificOrchestrator = new(); @@ -80,13 +80,16 @@ public CsharpMutantOrchestrator(MutantPlacer placer, IEnumerable mutat }); } - private static List DefaultMutatorList() => + public IEnumerable Mutators { get; } + + private List DefaultMutatorList() => new() { // the default list of mutators new BinaryExpressionMutator(), new BlockMutator(), new BooleanMutator(), + new DefaultParameterMutator(this, _options), new AssignmentExpressionMutator(), new PrefixUnaryMutator(), new PostfixUnaryMutator(), @@ -107,8 +110,6 @@ private static List DefaultMutatorList() => new IsPatternExpressionMutator() }; - private IEnumerable Mutators { get; } - public MutantPlacer Placer { get; } /// diff --git a/src/Stryker.Core/Stryker.Core/Mutants/ICsharpMutantOrchestrator.cs b/src/Stryker.Core/Stryker.Core/Mutants/ICsharpMutantOrchestrator.cs new file mode 100644 index 0000000000..7d43afe8ff --- /dev/null +++ b/src/Stryker.Core/Stryker.Core/Mutants/ICsharpMutantOrchestrator.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Stryker.Core.Mutators; + +namespace Stryker.Core.Mutants; + +public interface ICsharpMutantOrchestrator +{ + IEnumerable Mutators { get; } + MutantPlacer Placer { get; } + bool MustInjectCoverageLogic { get; } + ICollection Mutants { get; set; } + int MutantCount { get; set; } + + /// + /// Recursively mutates a single SyntaxNode + /// + /// The current root node + /// Mutated node + SyntaxNode Mutate(SyntaxNode input, SemanticModel semanticModel); + + /// + /// Gets the stored mutants and resets the mutant list to an empty collection + /// + IReadOnlyCollection GetLatestMutantBatch(); +} diff --git a/src/Stryker.Core/Stryker.Core/Mutators/DefaultParameterMutator.cs b/src/Stryker.Core/Stryker.Core/Mutators/DefaultParameterMutator.cs new file mode 100644 index 0000000000..04264eace2 --- /dev/null +++ b/src/Stryker.Core/Stryker.Core/Mutators/DefaultParameterMutator.cs @@ -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 +{ + private readonly ICsharpMutantOrchestrator _orchestrator; + private readonly StrykerOptions _options; + + public DefaultParameterMutator(ICsharpMutantOrchestrator orchestrator, StrykerOptions options) + { + _orchestrator = orchestrator; + _options = options; + } + + public override IEnumerable ApplyMutations(InvocationExpressionSyntax node, SemanticModel semanticModel) + { + var methodSymbol = (IMethodSymbol)semanticModel.GetSymbolInfo(node).Symbol; + if (methodSymbol is null) + { + yield break; + } + var parameterSymbols = methodSymbol.Parameters; + + var parametersWithDefaultValues = parameterSymbols.Where(p => p.HasExplicitDefaultValue); + + var overridenDefaultParameters = new List(); + 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; + + private IEnumerable MutateDefaultValueNode(SyntaxNode defaultValueNode, SemanticModel semanticModel) + { + List 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; + } +} diff --git a/src/Stryker.Core/Stryker.Core/Mutators/Mutator.cs b/src/Stryker.Core/Stryker.Core/Mutators/Mutator.cs index d12fc46a99..f87024ca79 100644 --- a/src/Stryker.Core/Stryker.Core/Mutators/Mutator.cs +++ b/src/Stryker.Core/Stryker.Core/Mutators/Mutator.cs @@ -42,7 +42,9 @@ public enum Mutator [MutatorDescription("Null coalescing")] NullCoalescing, [MutatorDescription("Math methods")] - Math + Math, + [MutatorDescription("Default parameter")] + DefaultParameter } public static class EnumExtension