Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public async Task CSharp_NetCore_SingleTestProject()

var report = await strykerRunOutput.DeserializeJsonReportAsync();

CheckReportMutants(report, total: 660, ignored: 271, survived: 4, killed: 9, timeout: 2, nocoverage: 340);
CheckReportMutants(report, total: 660, ignored: 271, survived: 4, killed: 9, timeout: 2, nocoverage: 344);
CheckReportTestCounts(report, total: 11);
}

Expand All @@ -101,7 +101,7 @@ public async Task CSharp_NetCore_WithTwoTestProjects()

var report = await strykerRunOutput.DeserializeJsonReportAsync();

CheckReportMutants(report, total: 660, ignored: 117, survived: 5, killed: 11, timeout: 2, nocoverage: 491);
CheckReportMutants(report, total: 660, ignored: 117, survived: 5, killed: 11, timeout: 2, nocoverage: 495);
CheckReportTestCounts(report, total: 21);
}

Expand All @@ -121,7 +121,7 @@ public async Task CSharp_NetCore_MSTestMTP()

var report = await strykerRunOutput.DeserializeJsonReportAsync();

CheckReportMutants(report, total: 660, ignored: 271, survived: 2, killed: 1, timeout: 2, nocoverage: 350);
CheckReportMutants(report, total: 660, ignored: 271, survived: 2, killed: 1, timeout: 2, nocoverage: 354);
}

[Fact]
Expand All @@ -140,7 +140,7 @@ public async Task CSharp_NetCore_XUnitMTP()

var report = await strykerRunOutput.DeserializeJsonReportAsync();

CheckReportMutants(report, total: 660, ignored: 271, survived: 1, killed: 1, timeout: 0, nocoverage: 353);
CheckReportMutants(report, total: 660, ignored: 271, survived: 1, killed: 1, timeout: 0, nocoverage: 357);
}

[Fact]
Expand All @@ -159,7 +159,7 @@ public async Task CSharp_NetCore_NUnitMTP()

var report = await strykerRunOutput.DeserializeJsonReportAsync();

CheckReportMutants(report, total: 660, ignored: 271, survived: 1, killed: 1, timeout: 0, nocoverage: 353);
CheckReportMutants(report, total: 660, ignored: 271, survived: 1, killed: 1, timeout: 0, nocoverage: 357);
}

[Fact]
Expand All @@ -178,7 +178,7 @@ public async Task CSharp_NetCore_TUnit()

var report = await strykerRunOutput.DeserializeJsonReportAsync();

CheckReportMutants(report, total: 660, ignored: 271, survived: 1, killed: 1, timeout: 0, nocoverage: 353);
CheckReportMutants(report, total: 660, ignored: 271, survived: 1, killed: 1, timeout: 0, nocoverage: 357);
}

[Fact]
Expand All @@ -197,7 +197,7 @@ public async Task CSharp_NetCore_MTPSolution()

var report = await strykerRunOutput.DeserializeJsonReportAsync();

CheckReportMutants(report, total: 670, ignored: 274, survived: 1, killed: 1, timeout: 0, nocoverage: 360);
CheckReportMutants(report, total: 670, ignored: 274, survived: 1, killed: 1, timeout: 0, nocoverage: 364);
CheckReportTestCounts(report, total: 0); // MTP doesn't report tests yet
}

Expand Down Expand Up @@ -237,7 +237,7 @@ public async Task CSharp_NetCore_SolutionRun()

var report = await strykerRunOutput.DeserializeJsonReportAsync();

CheckReportMutants(report, total: 660, ignored: 271, survived: 4, killed: 9, timeout: 2, nocoverage: 340);
CheckReportMutants(report, total: 660, ignored: 271, survived: 4, killed: 9, timeout: 2, nocoverage: 344);
CheckReportTestCounts(report, total: 23);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.VisualStudio.TestTools.UnitTesting;
Expand Down Expand Up @@ -69,4 +71,67 @@ public void ShouldNotMutateEmptyInitializer(string initializer)

result.ShouldBeEmpty();
}

[TestMethod]
[DataRow("public required int Value { get; set; }")]
[DataRow("public required int Value;")]
[DataRow("public int Other { get; set; } public required int Value { get; set; }")]
public void ShouldPreserveRequiredMembersWhenMutatingObjectInitializer(string members)
{
var (semanticModel, expression) = CreateSemanticModel(
$$"""
class Target { {{members}} }
class Caller { void M() { var t = new Target { Value = 1 }; } }
""");

var result = new ObjectCreationMutator().ApplyMutations(expression, semanticModel).ToList();

var mutation = result.ShouldHaveSingleItem();
mutation.Type.ShouldBe(Mutator.Initializer);
var replacement = mutation.ReplacementNode.ShouldBeOfType<ObjectCreationExpressionSyntax>();
replacement.Initializer.Expressions.ShouldHaveSingleItem().ToString().ShouldBe("Value=default!");
}

[TestMethod]
public void ShouldPreserveRequiredMembersFromBaseTypeWhenMutatingObjectInitializer()
{
var (semanticModel, expression) = CreateSemanticModel(
"""
class Base { public required int Value { get; set; } }
class Derived : Base { public int Other { get; set; } }
class Caller { void M() { var t = new Derived { Value = 1, Other = 2 }; } }
""");

var result = new ObjectCreationMutator().ApplyMutations(expression, semanticModel).ToList();

var mutation = result.ShouldHaveSingleItem();
var replacement = mutation.ReplacementNode.ShouldBeOfType<ObjectCreationExpressionSyntax>();
replacement.Initializer.Expressions.ShouldHaveSingleItem().ToString().ShouldBe("Value=default!");
}

[TestMethod]
public void ShouldMutateObjectInitializerWhenTypeHasNoRequiredMembers()
{
var (semanticModel, expression) = CreateSemanticModel(
"""
class Target { public int Value { get; set; } }
class Caller { void M() { var t = new Target { Value = 1 }; } }
""");

var result = new ObjectCreationMutator().ApplyMutations(expression, semanticModel).ToList();

result.ShouldHaveSingleItem().Type.ShouldBe(Mutator.Initializer);
}

private static (SemanticModel semanticModel, ObjectCreationExpressionSyntax expression) CreateSemanticModel(string source)
{
var syntaxTree = CSharpSyntaxTree.ParseText(source);
var compilation = CSharpCompilation.Create("TestAssembly")
.WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
.AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location))
.AddSyntaxTrees(syntaxTree);
var semanticModel = compilation.GetSemanticModel(syntaxTree);
var expression = syntaxTree.GetRoot().DescendantNodes().OfType<ObjectCreationExpressionSyntax>().Single();
return (semanticModel, expression);
}
}
49 changes: 48 additions & 1 deletion src/Stryker.Core/Stryker.Core/Mutators/ObjectCreationMutator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Expand All @@ -25,13 +26,59 @@ public override IEnumerable<Mutation> ApplyMutations(ObjectCreationExpressionSyn
}
if (node.Initializer?.Kind() == SyntaxKind.ObjectInitializerExpression && node.Initializer.Expressions.Count > 0)
{
// An empty initializer would fail to compile with CS9035 when the type has
// required members, so we preserve them by assigning `default!` to each.
var requiredMembers = GetRequiredMemberNames(node, semanticModel);
var replacementInitializer = requiredMembers.Count == 0
? SyntaxFactory.InitializerExpression(SyntaxKind.ObjectInitializerExpression)
: SyntaxFactory.InitializerExpression(
SyntaxKind.ObjectInitializerExpression,
SyntaxFactory.SeparatedList<ExpressionSyntax>(
requiredMembers.Select(name =>
SyntaxFactory.AssignmentExpression(
SyntaxKind.SimpleAssignmentExpression,
SyntaxFactory.IdentifierName(name),
SyntaxFactory.PostfixUnaryExpression(
SyntaxKind.SuppressNullableWarningExpression,
SyntaxFactory.LiteralExpression(SyntaxKind.DefaultLiteralExpression))))));

yield return new Mutation()
{
OriginalNode = node,
ReplacementNode = node.ReplaceNode(node.Initializer, SyntaxFactory.InitializerExpression(SyntaxKind.ObjectInitializerExpression)).WithCleanTrivia(),
ReplacementNode = node.ReplaceNode(node.Initializer, replacementInitializer).WithCleanTrivia(),
DisplayName = "Object initializer mutation",
Type = Mutator.Initializer,
};
}
}

private static IReadOnlyList<string> GetRequiredMemberNames(ObjectCreationExpressionSyntax node, SemanticModel semanticModel)
{
if (semanticModel is null)
{
return [];
}

if (semanticModel.GetTypeInfo(node).Type is not INamedTypeSymbol typeSymbol)
{
return [];
}

var names = new List<string>();
var seen = new HashSet<string>();
for (var current = typeSymbol; current is not null; current = current.BaseType)
{
foreach (var member in current.GetMembers())
{
var isRequired = (member is IPropertySymbol prop && prop.IsRequired) ||
(member is IFieldSymbol field && field.IsRequired);
if (isRequired && seen.Add(member.Name))
{
names.Add(member.Name);
}
}
}

return names;
}
}
Loading