diff --git a/docs/mutations.md b/docs/mutations.md index 4fe129a1b..8afe59879 100644 --- a/docs/mutations.md +++ b/docs/mutations.md @@ -264,3 +264,9 @@ For the full list of all available regex mutations, see the [regex mutator docs] |---------------------|---------------------| | `x ? a : b` | `true ? a : b` | | `x ? a : b` | `false ? a : b` | + +## Collection Expressions (_collectionexpression_) +| Original | Mutated | +|---------------------|-------------| +| `[]` | `[default]` | +| `[1, 2, 3]` | `[]` | \ No newline at end of file diff --git a/integrationtest/Validation/ValidationProject/ValidateStrykerResults.cs b/integrationtest/Validation/ValidationProject/ValidateStrykerResults.cs index fa2f062aa..788657b66 100644 --- a/integrationtest/Validation/ValidationProject/ValidateStrykerResults.cs +++ b/integrationtest/Validation/ValidationProject/ValidateStrykerResults.cs @@ -83,7 +83,7 @@ public async Task CSharp_NetCore_SingleTestProject() var report = await JsonReportSerialization.DeserializeJsonReportAsync(strykerRunOutput); - CheckReportMutants(report, total: 589, ignored: 246, survived: 4, killed: 9, timeout: 2, nocoverage: 297); + CheckReportMutants(report, total: 601, ignored: 247, survived: 4, killed: 9, timeout: 2, nocoverage: 308); CheckReportTestCounts(report, total: 11); } @@ -122,7 +122,7 @@ public async Task CSharp_NetCore_WithTwoTestProjects() var report = await JsonReportSerialization.DeserializeJsonReportAsync(strykerRunOutput); - CheckReportMutants(report, total: 589, ignored: 105, survived: 5, killed: 11, timeout: 2, nocoverage: 435); + CheckReportMutants(report, total: 601, ignored: 105, survived: 5, killed: 11, timeout: 2, nocoverage: 447); CheckReportTestCounts(report, total: 21); } @@ -141,7 +141,7 @@ public async Task CSharp_NetCore_SolutionRun() var report = await JsonReportSerialization.DeserializeJsonReportAsync(strykerRunOutput); - CheckReportMutants(report, total: 589, ignored: 246, survived: 4, killed: 9, timeout: 2, nocoverage: 297); + CheckReportMutants(report, total: 601, ignored: 247, survived: 4, killed: 9, timeout: 2, nocoverage: 308); CheckReportTestCounts(report, total: 23); } diff --git a/src/Stryker.Abstractions/Mutator.cs b/src/Stryker.Abstractions/Mutator.cs index 6b6f286be..7080541a9 100644 --- a/src/Stryker.Abstractions/Mutator.cs +++ b/src/Stryker.Abstractions/Mutator.cs @@ -41,5 +41,7 @@ public enum Mutator [MutatorDescription("String Method")] StringMethod, [MutatorDescription("Conditional operators")] - Conditional + Conditional, + [MutatorDescription("Collection expressions")] + CollectionExpression } diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutants/CsharpMutantOrchestratorTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutants/CsharpMutantOrchestratorTests.cs index af8c38040..b5ba19af9 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Mutants/CsharpMutantOrchestratorTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Mutants/CsharpMutantOrchestratorTests.cs @@ -1,3 +1,6 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; using System.Linq; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -1948,4 +1951,269 @@ public void ShouldIncrementMutantCountUniquely() secondMutant.Id.ShouldBe(firstMutant.Id + 1); } + + [TestMethod] + public void ShouldMutateCollectionExpressionSpanProperty() + { + var source = "static ReadOnlySpan Value => [1, 2, 3];"; + + var expected = + "static ReadOnlySpan Value => (StrykerNamespace.MutantControl.IsActive(0)?[]:[1,2,3]);"; + ShouldMutateSourceInClassToExpected(source, expected); + } + + [TestMethod] + public void ShouldMutateCollectionExpressionLocalsInMethod() + { + var source = """ + public void M() { + int[] abc = { 5, 5 }; + int[] bcd = [1, .. abc, 3]; + } + """; + + var expected = + """ + public void M() { + if (StrykerNamespace.MutantControl.IsActive(0)) { } + else{ + if(StrykerNamespace.MutantControl.IsActive(1)){ + int[] abc = {}; + int[] bcd = [1, .. abc, 3]; + } else { + int[] abc = { 5, 5 }; + int[] bcd = (StrykerNamespace.MutantControl.IsActive(2)?[]:[1, .. abc, 3]); + } + } + } + """; + ShouldMutateSourceInClassToExpected(source, expected); + } + + [TestMethod] + public void ShouldMutateImplicitCollectionExpressionInMethod() + { + var source = """ + public void M() { + int[] abc = { 5, 5 }; + var bcd = (int[])[1, .. abc, 3]; + } + """; + + var expected = + """ + public void M() { + if (StrykerNamespace.MutantControl.IsActive(0)) { + } else { + if (StrykerNamespace.MutantControl.IsActive(1)) { + int[] abc = {}; + var bcd = (int[])[1, ..abc, 3]; + } else { + int[] abc = {5, 5}; + var bcd = (int[])( + StrykerNamespace.MutantControl.IsActive(2) ? [] : [ 1, ..abc, 3 ]); + } + } + } + """; + ShouldMutateSourceInClassToExpected(source, expected); + } + + [TestMethod] + public void ShouldMutateUsedCollectionExpression() + { + var source = """ + public void M() { + // Stryker disable String : Not mutation under test + Span weekDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + } + """; + + var expected = + """ + public void M() { + if (StrykerNamespace.MutantControl.IsActive(0)) { + } else { + // Stryker disable String : Not mutation under test + Span weekDays = (StrykerNamespace.MutantControl.IsActive(1) ? [] : ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]); + } + } + """; + ShouldMutateSourceInClassToExpected(source, expected); + } + + [TestMethod] + public void ShouldMutateCollectionExpressionInManyForms() + { + var source = """ + // Initialize private field: + // Stryker disable String : Not mutation under test + private static readonly ImmutableArray _months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + // property with expression body: + public IEnumerable MaxDays => + [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + public int Sum(IEnumerable values) => + values.Sum(); + public void Example() + { + // As a parameter: + int sum = Sum([1, 2, 3, 4, 5]); + } + """; + + var expected = + """ + // Initialize private field: + // Stryker disable String : Not mutation under test + private static readonly ImmutableArray _months = + StrykerNamespace.MutantContext.TrackValue( + () =>(StrykerNamespace.MutantControl.IsActive(0) ? [] : ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"])); + + // property with expression body: + public IEnumerable MaxDays => + (StrykerNamespace.MutantControl.IsActive(13) + ? [] + : [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ]); + + public int Sum(IEnumerable values) => + (StrykerNamespace.MutantControl.IsActive(14) ? values.Max() : values.Sum()); + + public void Example() { + if (StrykerNamespace.MutantControl.IsActive(15)) { + } else { + // As a parameter: + int sum = Sum( + (StrykerNamespace.MutantControl.IsActive(16) ? [] : [ 1, 2, 3, 4, 5 ])); + } + } + """; + ShouldMutateSourceInClassToExpected(source, expected); + } + + [TestMethod] + public void ShouldMutateNonConstatCollectionExpression() + { + var source = """ + public void M() { + // Stryker disable String : Not mutation under test + string hydrogen = "H"; + string helium = "He"; + string lithium = "Li"; + string beryllium = "Be"; + string boron = "B"; + string carbon = "C"; + string nitrogen = "N"; + string oxygen = "O"; + string fluorine = "F"; + string neon = "Ne"; + string[] elements = [hydrogen, helium, lithium, beryllium, boron, carbon, nitrogen, oxygen, fluorine, neon]; + } + """; + + var expected = + """ + public void M() { + // Stryker disable String : Not mutation under test + if (StrykerNamespace.MutantControl.IsActive(0)) { + } else { + string hydrogen = "H"; + string helium = "He"; + string lithium = "Li"; + string beryllium = "Be"; + string boron = "B"; + string carbon = "C"; + string nitrogen = "N"; + string oxygen = "O"; + string fluorine = "F"; + string neon = "Ne"; + string[] elements = (StrykerNamespace.MutantControl.IsActive(11) ? [] : [ + hydrogen, helium, lithium, beryllium, boron, carbon, nitrogen, oxygen, + fluorine, neon + ]); + } + } + """; + ShouldMutateSourceInClassToExpected(source, expected); + } + + [TestMethod] + public void ShouldMutateSpreadCollectionExpression() + { + var source = """ + public void M() { + // Stryker disable String : Not mutation under test + string[] vowels = ["a", "e", "i", "o", "u"]; + string[] consonants = ["b", "c", "d", "f", "g", "h", "j", "k", "l", "m", + "n", "p", "q", "r", "s", "t", "v", "w", "x", "z"]; + string[] alphabet = [.. vowels, .. consonants, "y"]; + } + """; + + var expected = + """ + public void M() { + // Stryker disable String : Not mutation under test + if (StrykerNamespace.MutantControl.IsActive(0)) { + } else { + string[] vowels = (StrykerNamespace.MutantControl.IsActive(1) ? [] : ["a", "e", "i", "o", "u"]); + string[] consonants = (StrykerNamespace.MutantControl.IsActive(7) ? [] : [ + "b", "c", "d", "f", "g", "h", "j", "k", "l", "m", + "n", "p", "q", "r", "s", "t", "v", "w", "x", "z" + ]); + string[] alphabet = + (StrykerNamespace.MutantControl.IsActive(28) + ? [] + : [..vowels, ..consonants, "y"]); + } + } + """; + ShouldMutateSourceInClassToExpected(source, expected); + } + + [TestMethod] + public void ShouldMutateImplicitCollectionExpressionParameter() + { + var source = """ + public void M() { + Iter([1]); + } + public IEnumerable Iter(IList list) { } + """; + + var expected = + """ + public void M() { + if (StrykerNamespace.MutantControl.IsActive(0)) { + } else { + if (StrykerNamespace.MutantControl.IsActive(1)) { + ; + } else { + Iter((StrykerNamespace.MutantControl.IsActive(2) ? [] : [ 1 ])); + } + } + } + public IEnumerable Iter(IList list) { } + """; + ShouldMutateSourceInClassToExpected(source, expected); + } + + [TestMethod] + public void ShouldMutateNestedImplicitCollectionExpression() + { + var source = "static int[][] Value => [[1, 2], [3]];"; + + var expected = + "static int[][] Value => (StrykerNamespace.MutantControl.IsActive(0)?[]:[(StrykerNamespace.MutantControl.IsActive(1)?[]:[1, 2]), (StrykerNamespace.MutantControl.IsActive(2)?[]:[3])]);"; + ShouldMutateSourceInClassToExpected(source, expected); + } + + [TestMethod] + public void ShouldMutateNestedExplicitCollectionExpression() + { + var source = "static int[][] Value => [[1, 2], new int[] { 3 }];"; + + var expected = + "static int[][] Value => (StrykerNamespace.MutantControl.IsActive(0)?[]:[(StrykerNamespace.MutantControl.IsActive(1)?[]:[1, 2]), (StrykerNamespace.MutantControl.IsActive(2)?new int[] {}:new int[] { 3 })]);"; + ShouldMutateSourceInClassToExpected(source, expected); + } } diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/CollectionExpressionMutatorTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/CollectionExpressionMutatorTests.cs new file mode 100644 index 000000000..b48508e8c --- /dev/null +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/CollectionExpressionMutatorTests.cs @@ -0,0 +1,524 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System; +using System.Linq; +using System.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using Stryker.Abstractions.Exceptions; +using Stryker.Abstractions; +using Stryker.Abstractions.Mutators; +using Stryker.Abstractions.Options; +using Stryker.Core.Compiling; +using Stryker.Core.InjectedHelpers; +using Stryker.Core.Mutants; +using Stryker.Core.MutationTest; +using Stryker.Core.Mutators; +using Stryker.Core.ProjectComponents.SourceProjects; +using System.Text; + +namespace Stryker.Core.UnitTest.Mutators; + +[TestClass] +public class CollectionExpressionMutatorTests : TestBase +{ + [TestMethod] + public void ShouldBeMutationLevelAdvanced() + { + var target = new CollectionExpressionMutator(); + target.MutationLevel.ShouldBe(MutationLevel.Advanced); + } + + [TestMethod] + [DataRow("[]")] + [DataRow("[ ]")] + [DataRow("[ ]")] + [DataRow("[ /* Comment */ ]")] + public void ShouldAddValueToEmptyCollectionExpression(string expression) + { + var expressionSyntax = SyntaxFactory.ParseExpression(expression) as CollectionExpressionSyntax; + var target = new CollectionExpressionMutator(); + var result = target.ApplyMutations(expressionSyntax, null); + + var mutation = result.ShouldHaveSingleItem(); + mutation.DisplayName.ShouldBe("Collection expression mutation"); + var replacement = mutation.ReplacementNode.ShouldBeOfType(); + var element = replacement.Elements.ShouldHaveSingleItem(); + element.ShouldBeOfType().Expression.ShouldBeOfType().Token.IsKind(SyntaxKind.DefaultKeyword).ShouldBeTrue(); + } + + [TestMethod] + [DataRow("[1, 2, 3]")] + [DataRow("[-1, 3]")] + [DataRow("[1, .. abc, 3]")] + [DataRow("[..abc]")] + public void ShouldRemoveValuesFromCollectionExpression(string expression) + { + var expressionSyntax = SyntaxFactory.ParseExpression(expression) as CollectionExpressionSyntax; + var target = new CollectionExpressionMutator(); + var result = target.ApplyMutations(expressionSyntax, null); + var mutation = result.ShouldHaveSingleItem(); + mutation.DisplayName.ShouldBe("Collection expression mutation"); + var replacement = mutation.ReplacementNode.ShouldBeOfType(); + replacement.Elements.ShouldBeEmpty(); + } + + [TestMethod] + [CollectionExpressionTest("Should mutate collection expression with spread elements", + """ + using System; + + namespace ExampleProject; + + class ClassName { + public void M() { + int[] abc = [ 1, 5, 7 ]; + int[] bcd = [ 1, ..abc, 3 ]; + } + } + """, 2)] + [CollectionExpressionTest("Should mutate collection expression with explicit cast", + """ + using System; + + namespace ExampleProject; + + class ClassName { + public void M() { + int[] abc = [ 1, 5, 7 ]; + var bcd = (int[])[ 1, ..abc, 3 ]; + } + } + """, 2)] + [CollectionExpressionTest("Should mutate nested collection expression", + """ + using System; + + namespace ExampleProject; + + class ClassName { + public void M() { + int[][] abc = [ [ 1, 5 ], [ 7 ] ]; + } + } + """, 3)] + [CollectionExpressionTest("Should mutate collection expression with inner array initialization", + """ + using System; + + namespace ExampleProject; + + class ClassName { + public void M() { + int[][] abc = [ [ 1, 5 ], new [] { 7 } ]; + } + } + """, 2)] + [CollectionExpressionTest("Should mutate collection expression with inner explicit spread collection expression", + """ + using System; + + namespace ExampleProject; + + class ClassName { + public void M() { + int[] abc = [ ..(Span)[ 1, 5 ], ..(Span)[ 7 ] ]; + } + } + """, 3)] + [CollectionExpressionTest("Should mutate empty collection expression", + """ + using System; + + namespace ExampleProject; + + class ClassName { + public void M() { + int[] abc = []; + } + } + """, 1)] + [CollectionExpressionTest("Should mutated collection expression used as a generic parameter", + """ + using System; + using System.Collections.Generic; + + namespace ExampleProject; + + class ClassName { + public IEnumerable M() => Iter([ 1 ]); + + public IEnumerable Iter(IList list) { + foreach (var l in list) { + yield return l; + } + } + } + """, 1)] + [CollectionExpressionTest("Empty collection expression mutation should not be ambiguous", + """ + using System; + using System.Collections.Generic; + + namespace ExampleProject; + + class ClassName { + public IEnumerable M() => Iter([ 1 ]); + + public IEnumerable Iter(IList list) { + foreach (var l in list) { + yield return l; + } + } + + public IEnumerable Iter(IReadOnlyCollection list) { + foreach (var l in list) { + yield return l; + } + } + + public IEnumerable Iter(T[] list) { + foreach (var l in list) { + yield return l; + } + } + + public IEnumerable Iter(ReadOnlyMemory list) { + for (var i = 0; i < list.Length; i++) { + yield return list.Span[i]; + } + } + } + """, 1)] + [CollectionExpressionTest("Filled collection expression mutation should not be ambiguous", + """ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + + namespace ExampleProject; + + class ClassName { + public void M() { + ImmutableArray? a = []; + var b = a ?? []; + } + } + """, 2)] + [CollectionExpressionTest("Empty collection expression mutation should not be ambiguous again", + """ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + + namespace ExampleProject; + + class ClassName { + public void M() { + List? a = []; + var b = a ?? []; + } + } + """, 2)] + [CollectionExpressionTest("Should mutate collection expression with varying sources", + """ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + + namespace ExampleProject; + + class ClassName { + public string[] M() { + string[] vowels = [ "a", "e", "i", "o", "u" ]; + string[] consonants = [ + "b", "c", "d", "f", "g", "h", "j", "k", "l", "m", + "n", "p", "q", "r", "s", "t", "v", "w", "x", "z" + ]; + string[] alphabet = [..vowels, ..consonants, "y" ]; + return alphabet; + } + } + """, 3)] + [CollectionExpressionTest("Should mutate collection expression when nullable", + """ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Linq; + using System.IO; + + namespace ExampleProject; + + class ClassName { + public void GetLocalDateTime(Stream s) { + AddAll(Deserialize(s, Enumerable.Empty()) ?? []); + } + public IEnumerable? Deserialize(Stream s, IEnumerable s2) { + return []; + } + public void AddAll(IEnumerable list) { } + } + """, 2)] + [CollectionExpressionTest("Should mutate heavily nested collection expression", + """ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Linq; + using System.IO; + + namespace ExampleProject; + + class ClassName { + public static int[][][][][] Deep => [[[[[]]]]]; + } + """, 5)] + [CollectionExpressionTest("Should mutate empty collection expressions", + """ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Linq; + using System.IO; + + namespace ExampleProject; + + class ClassName { + // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-12.0/collection-expressions#empty-collection-literal + public static void Method() { + int[] x = []; + IEnumerable y = []; + List z = []; + } + } + """, 3)] + [CollectionExpressionTest("Should support ref safety", + """ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Linq; + using System.IO; + + namespace ExampleProject; + + class ClassName { + // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-12.0/collection-expressions#ref-safety + static ReadOnlySpan AsSpanConstants() + { + return [1, 2, 3]; // ok: span refers to assembly data section + } + + static ReadOnlySpan AsSpan3(T x, T y, T z) + { + return (T[])[x, y, z]; // ok: span refers to T[] on heap + } + } + """, 2)] + [CollectionExpressionTest("Should support type inference", + """ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Linq; + using System.IO; + + namespace ExampleProject; + + class ClassName { + // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-12.0/collection-expressions#type-inference + public static void Method() { + var a = AsArray([1, 2, 3]); // AsArray(int[]) + var b = AsListOfArray([[4, 5], []]); // AsListOfArray(List) + + static T[] AsArray(T[] arg) => arg; + static List AsListOfArray(List arg) => arg; + } + } + """, 4)] + [CollectionExpressionTest("Should support overload resolution", + """ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Linq; + using System.IO; + + namespace ExampleProject; + + class ClassName { + static void Generic(Span value) { } + static void Generic(T[] value) { } + + static void SpanDerived(Span value) { } + static void SpanDerived(object[] value) { } + + static void ArrayDerived(Span value) { } + static void ArrayDerived(string[] value) { } + + // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-12.0/collection-expressions#overload-resolution + public static void Method() { + // Array initializers + Generic(new[] { "" }); // string[] + ArrayDerived(new[] { "" }); // string[] + + // Collection expressions + Generic([""]); // Span + SpanDerived([""]); // Span + } + } + """, 2)] + [CollectionExpressionTest("Should not result in syntax ambiguities", + """ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Linq; + using System.IO; + + namespace ExampleProject; + + class ClassName { + static void Generic(Span value) { } + static void Generic(T[] value) { } + + static void SpanDerived(Span value) { } + static void SpanDerived(object[] value) { } + + static void ArrayDerived(Span value) { } + static void ArrayDerived(string[] value) { } + + // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-12.0/collection-expressions#syntax-ambiguities + public static void Method() { + Range range1 = default; + Range range2 = default; + int e = 3; + Range[] ranges = [range1, (..e), range2]; + } + } + """, 1)] + [CollectionExpressionTest("Should support int to long implicit conversion", + """ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Linq; + using System.IO; + + namespace ExampleProject; + + class ClassName { + static void Generic(Span value) { } + static void Generic(T[] value) { } + + static void SpanDerived(Span value) { } + static void SpanDerived(object[] value) { } + + static void ArrayDerived(Span value) { } + static void ArrayDerived(string[] value) { } + + // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-12.0/collection-expressions#resolved-questions + public static void Method() { + void DoWork(IEnumerable values) { } + // Needs to produce `longs` not `ints` for this to work. + DoWork([1, 2, 3]); + } + } + """, 1)] + public void MutatedCollectionExpressionsShouldCompile(string inputText, int expectedMutants) + { + var syntaxTree = CSharpSyntaxTree.ParseText(inputText); + + var compilation = CSharpCompilation.Create("TestAssembly") + .WithOptions(new + CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, + nullableContextOptions: NullableContextOptions.Enable)) + .AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly + .Location)) + .AddSyntaxTrees(syntaxTree); + var semanticModel = compilation.GetSemanticModel(syntaxTree); + + var injector = new CodeInjection(); + var orchestrator = new CsharpMutantOrchestrator(new MutantPlacer(injector), + options: new StrykerOptions + { + MutationLevel = MutationLevel.Complete, + OptimizationMode = OptimizationModes.CoverageBasedTest, + ExcludedMutations = Enum.GetValues() + .Except([Mutator.CollectionExpression]) + }); + syntaxTree = orchestrator.Mutate(syntaxTree, semanticModel); + orchestrator.Mutants.Count(a => a.Mutation.Type == Mutator.CollectionExpression).ShouldBe(expectedMutants); + + List references = + [ + typeof(object).Assembly.Location, + typeof(List).Assembly.Location, + typeof(Enumerable).Assembly.Location, + typeof(ImmutableArray<>).Assembly.Location, + typeof(ValueType).Assembly.Location, + ..Assembly.GetEntryAssembly()?.GetReferencedAssemblies().Select(a => Assembly.Load(a).Location) ?? [] + ]; + + var input = new MutationTestInput + { + SourceProjectInfo = new SourceProjectInfo + { + AnalyzerResult = TestHelper.SetupProjectAnalyzerResult(projectFilePath: "/c/project.csproj", + properties: new Dictionary + { + { "TargetDir", "" }, + { "AssemblyName", "AssemblyName" }, + { + "TargetFileName", + "TargetFileName.dll" + } + }, + references: references.ToArray() + ) + .Object + } + }; + + var target = new CsharpCompilingProcess(input); + + try + { + var result = + target.Compile([ + syntaxTree, + ..injector.MutantHelpers.Select(a => CSharpSyntaxTree.ParseText(a.Value, path: a.Key, + encoding: Encoding.UTF32)) + ], + Stream.Null, Stream.Null); + result.Success.ShouldBe(true); + result.RollbackedIds.ShouldBeEmpty(); + } + catch (CompilationException) + { + Assert.Fail($"Compilation failed with code: {syntaxTree}"); + } + } +} + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +file class CollectionExpressionTestAttribute : DataRowAttribute +{ + /// + public CollectionExpressionTestAttribute(string testName, string inputCode, int mutationCount) : + base(inputCode, mutationCount) => + DisplayName = testName; +} diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/NullCoalescingExpressionMutatorTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/NullCoalescingExpressionMutatorTests.cs index 180680cff..2f6fe5135 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/NullCoalescingExpressionMutatorTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/NullCoalescingExpressionMutatorTests.cs @@ -104,4 +104,35 @@ public void ShouldMutateIfBothSidesAreNullable() result.Count.ShouldBe(3); } + + [TestMethod] + public void ShouldMutateCollectionExpressions() + { + var syntaxTree = CSharpSyntaxTree.ParseText( + """ + public void GetLocalDateTime(Stream s) + { + AddAll(Deserialize(s, Enumerable.Empty()) ?? []) + } + public void AddAll(IEnumerable list) + { + + } + public IEnumerable? Deserialize(Stream s, IEnumerable s2) { + return []; + } + """); + + var compilation = CSharpCompilation.Create("TestAssembly") + .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, + nullableContextOptions: NullableContextOptions.Enable)) + .AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly + .Location)) + .AddSyntaxTrees(syntaxTree); + var semanticModel = compilation.GetSemanticModel(syntaxTree); + var expression = syntaxTree.GetRoot().DescendantNodes().OfType().First(); + var target = new NullCoalescingExpressionMutator(); + var result = target.ApplyMutations(expression, semanticModel).ToList(); + result.Count.ShouldBe(2); + } } 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 e5ec70319..b4825e467 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Options/Inputs/IgnoreMutationsInputTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Options/Inputs/IgnoreMutationsInputTests.cs @@ -30,7 +30,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, StringMethod, Conditional]"); + 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, StringMethod, Conditional, CollectionExpression]"); } [TestMethod] diff --git a/src/Stryker.Core/Stryker.Core/Mutants/CsharpMutantOrchestrator.cs b/src/Stryker.Core/Stryker.Core/Mutants/CsharpMutantOrchestrator.cs index 4fb782c00..fadc06405 100644 --- a/src/Stryker.Core/Stryker.Core/Mutants/CsharpMutantOrchestrator.cs +++ b/src/Stryker.Core/Stryker.Core/Mutants/CsharpMutantOrchestrator.cs @@ -115,7 +115,8 @@ private static List DefaultMutatorList() => new MathMutator(), new SwitchExpressionMutator(), new IsPatternExpressionMutator(), - new StringMethodMutator() + new StringMethodMutator(), + new CollectionExpressionMutator() ]; private IEnumerable Mutators { get; } diff --git a/src/Stryker.Core/Stryker.Core/Mutators/CollectionExpressionMutator.cs b/src/Stryker.Core/Stryker.Core/Mutators/CollectionExpressionMutator.cs new file mode 100644 index 000000000..d39b44bba --- /dev/null +++ b/src/Stryker.Core/Stryker.Core/Mutators/CollectionExpressionMutator.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Stryker.Abstractions.Mutants; +using Stryker.Abstractions.Mutators; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Stryker.Core.Mutators; + +public sealed class CollectionExpressionMutator : MutatorBase +{ + public override MutationLevel MutationLevel => MutationLevel.Advanced; + + private static ExpressionElementSyntax DefaultElementSyntax => + ExpressionElement(LiteralExpression(SyntaxKind.DefaultLiteralExpression, Token(SyntaxKind.DefaultKeyword))); + + public override IEnumerable ApplyMutations(CollectionExpressionSyntax node, SemanticModel semanticModel) + { + if (node.Elements.Count > 0) + { + var type = semanticModel?.GetOperation(node)?.Type; + + yield return new Mutation + { + OriginalNode = node, + ReplacementNode = + type is not null + ? CastExpression(ParseTypeName(type.ToMinimalDisplayString(semanticModel, node.SpanStart)), + node.WithElements([])) + : node.WithElements([]), + DisplayName = "Collection expression mutation", + Type = Mutator.CollectionExpression + }; + } + else + { + yield return new Mutation + { + OriginalNode = node, + ReplacementNode = node.AddElements(DefaultElementSyntax), + DisplayName = "Collection expression mutation", + Type = Mutator.CollectionExpression + }; + } + } +} diff --git a/src/Stryker.Core/Stryker.Core/Mutators/NullCoalescingExpressionMutator.cs b/src/Stryker.Core/Stryker.Core/Mutators/NullCoalescingExpressionMutator.cs index a9f6804bc..afc2d4944 100644 --- a/src/Stryker.Core/Stryker.Core/Mutators/NullCoalescingExpressionMutator.cs +++ b/src/Stryker.Core/Stryker.Core/Mutators/NullCoalescingExpressionMutator.cs @@ -52,7 +52,7 @@ public override IEnumerable ApplyMutations(BinaryExpressionSyntax node } // Only create a "remove left" mutant if the right side is nullable. - if (IsNullable(node.Right, semanticModel)) + if (IsNullable(node.Right, semanticModel) || node.Right.IsKind(SyntaxKind.CollectionExpression)) { yield return new Mutation {