Skip to content
Closed
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
@@ -0,0 +1,36 @@
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Shouldly;
using Stryker.Core.Helpers;

namespace Stryker.Core.UnitTest.Helpers;

[TestClass]
public class RoslynHelperTests : TestBase
{
[TestMethod]
[DataRow("s is { Length: > 0 } region")]
[DataRow("s is var captured")]
[DataRow("s is int n")]
public void ContainsDeclarationsShouldDetectPatternDesignations(string expression)
{
SyntaxFactory.ParseExpression(expression).ContainsDeclarations().ShouldBeTrue();
}

[TestMethod]
public void ContainsDeclarationsShouldDetectOutVarDeclaration()
{
SyntaxFactory.ParseExpression("int.TryParse(s, out var value)")
.ContainsDeclarations().ShouldBeTrue();
}

[TestMethod]
[DataRow("s.Length > 0")]
[DataRow("s is [_, _]")]
[DataRow("(a, b)")]
public void ContainsDeclarationsShouldReturnFalseWithoutDeclarations(string expression)
{
SyntaxFactory.ParseExpression(expression).ContainsDeclarations().ShouldBeFalse();
}
}
60 changes: 60 additions & 0 deletions src/Stryker.Core/Stryker.Core.UnitTest/Mutators/LinqMutatorTest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +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 @@ -159,4 +160,63 @@ static void Main(string[] args)

result.ShouldHaveSingleItem().ReplacementNode.ShouldBeOfType<MemberAccessExpressionSyntax>().Name.ToString().ShouldBe("Any");
}

[TestMethod]
public void ShouldNotMutateAppendOnNonEnumerableReceiver()
{
var (semanticModel, memberAccess) = CreateSemanticModelForMemberAccess(
"""
namespace TestApp
{
interface IResponseCookies { void Append(string key, string value); }
class Program
{
static void Run(IResponseCookies cookies) => cookies.Append("k", "v");
}
}
""",
"Append");

var result = new LinqMutator().ApplyMutations(memberAccess, semanticModel);

result.ShouldBeEmpty();
}

[TestMethod]
public void ShouldMutateAppendOnEnumerableReceiver()
{
var (semanticModel, memberAccess) = CreateSemanticModelForMemberAccess(
"""
using System.Collections.Generic;
using System.Linq;
namespace TestApp
{
class Program
{
static void Run(IEnumerable<int> items) { var x = items.Append(1); }
}
}
""",
"Append");

var result = new LinqMutator().ApplyMutations(memberAccess, semanticModel).ToList();

result.ShouldHaveSingleItem();
}

private static (SemanticModel semanticModel, MemberAccessExpressionSyntax expression) CreateSemanticModelForMemberAccess(string source, string memberName)
{
var syntaxTree = CSharpSyntaxTree.ParseText(source);
var compilation = CSharpCompilation.Create("TestAssembly")
.WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
.AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location))
.AddReferences(MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location))
.AddReferences(MetadataReference.CreateFromFile(typeof(System.Collections.Generic.IEnumerable<>).Assembly.Location))
.AddSyntaxTrees(syntaxTree);
var semanticModel = compilation.GetSemanticModel(syntaxTree);
var memberAccess = syntaxTree.GetRoot().DescendantNodes()
.OfType<MemberAccessExpressionSyntax>()
.Single(x => x.Name.Identifier.ValueText == memberName);
return (semanticModel, memberAccess);
}
}
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,62 @@ 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 ShouldNotMutateObjectInitializerWhenTypeHasRequiredMembers(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);

result.ShouldBeEmpty();
}

[TestMethod]
public void ShouldNotMutateObjectInitializerWhenBaseTypeHasRequiredMembers()
{
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);

result.ShouldBeEmpty();
}

[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);
}
}
11 changes: 10 additions & 1 deletion src/Stryker.Core/Stryker.Core/Helpers/RoslynHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,16 @@ public static bool IsAStringExpression(this ExpressionSyntax node, SemanticModel
/// <returns>true if it contains a declaration</returns>
public static bool ContainsDeclarations(this SyntaxNode node) =>
node.ContainsNodeThatVerifies(x =>
x.IsKind(SyntaxKind.DeclarationExpression) || x.IsKind(SyntaxKind.DeclarationPattern), true);
x.IsKind(SyntaxKind.DeclarationExpression)
|| x.IsKind(SyntaxKind.DeclarationPattern)
// Recursive / var patterns can also introduce a variable via their
// SingleVariableDesignation, e.g. `s is { Length: > 0 } region`.
// Without this, expression-level mutations duplicate the declaration
// across ternary branches and produce CS0136. Scope to designations
// directly under a recursive/var pattern so unrelated designations
// (out var, tuple deconstruction) keep their previous behaviour.
|| (x.IsKind(SyntaxKind.SingleVariableDesignation)
&& x.Parent is RecursivePatternSyntax or VarPatternSyntax), true);

/// <summary>
/// Gets the return the type of the method (incl. constructor, destructor...)
Expand Down
8 changes: 8 additions & 0 deletions src/Stryker.Core/Stryker.Core/Mutants/MutationStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,14 @@ public ExpressionSyntax Inject(ExpressionSyntax mutatedNode, ExpressionSyntax so
// never inject at member access level, there is no known control structure
return mutatedNode;
}
// If this expression introduces a declaration (e.g. `x is { … } v`),
// duplicating it via a ternary would re-declare the variable in each
// branch and trigger CS0136. Forward mutations to an enclosing block
// so they can be controlled by an if-statement instead.
if (sourceNode.ContainsDeclarations())
{
return mutatedNode;
}
var store = _pendingMutations.Peek().Store;
var result = _mutantPlacer.PlaceExpressionControlledMutations(mutatedNode,
store.Select(m => (m, sourceNode.InjectMutation(m.Mutation))));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,17 @@ public void Mutate(MutationTestInput input, IStrykerOptions options)
var projectInfo = input.SourceProjectInfo.ProjectContents;
var orchestrator = new CsharpMutantOrchestrator(new MutantPlacer(input.SourceProjectInfo.CodeInjector), options: _options);
var compilingProcess = new CsharpCompilingProcess(input, options: _options);
var semanticModels = compilingProcess.GetSemanticModels(projectInfo.GetAllFiles().Cast<CsharpFileLeaf>().Select(x => x.SyntaxTree));
// Include auto-generated compilation trees (e.g. GlobalUsings.g.cs,
// AssemblyInfo.cs) so the semantic model resolves implicit usings and
// attributes — otherwise mutators that consult the semantic model see
// unresolved types. Files have not yet been mutated, so use the
// original SyntaxTree for each file rather than CompilationSyntaxTrees
// (which would return null MutatedSyntaxTrees at this stage).
var fileTrees = projectInfo.GetAllFiles().Cast<CsharpFileLeaf>().Select(x => x.SyntaxTree);
var generatedTrees = ((ProjectComponent<SyntaxTree>)projectInfo).CompilationSyntaxTrees
.Where(t => t is not null)
.Except(fileTrees);
var semanticModels = compilingProcess.GetSemanticModels(fileTrees.Concat(generatedTrees));

// Mutate source files
foreach (var file in projectInfo.GetAllFiles().Cast<CsharpFileLeaf>())
Expand Down
88 changes: 88 additions & 0 deletions src/Stryker.Core/Stryker.Core/Mutators/LinqMutator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Stryker.Core.Helpers;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Stryker.Core.Mutators;

Expand Down Expand Up @@ -109,6 +110,11 @@ public override IEnumerable<Mutation> ApplyMutations(ExpressionSyntax node, Sema
yield break;
}

if (!IsLinqInvocation(node, semanticModel))
{
yield break;
}

yield return new Mutation
{
DisplayName =
Expand All @@ -134,4 +140,86 @@ private static InvocationExpressionSyntax FindEnclosingInvocation(ExpressionSynt
}
return null;
}

// Ensures we only mutate calls that are actually LINQ operations. Without this
// check, any method whose name happens to match a LINQ operator (e.g.
// IResponseCookies.Append) would be incorrectly rewritten.
private static bool IsLinqInvocation(ExpressionSyntax node, SemanticModel semanticModel)
{
if (semanticModel is null)
{
// No semantic model available (e.g. unit tests) — preserve legacy
// name-only matching behaviour.
return true;
}

// `node` is the MemberAccess/MemberBinding being mutated; its parent is
// the enclosing InvocationExpression for a direct call (e.g.
// `x.Append(...)`). FindEnclosingInvocation only walks up through chained
// member access and returns null in the direct case.
var invocation = node.Parent as InvocationExpressionSyntax ?? FindEnclosingInvocation(node);
if (invocation is null)
{
return true;
}

if (semanticModel.GetSymbolInfo(invocation).Symbol is IMethodSymbol methodSymbol)
{
var containingType = methodSymbol.ContainingType?.ConstructedFrom ?? methodSymbol.ContainingType;
if (containingType is not null && IsLinqHostType(containingType))
{
return true;
}

var receiverType = methodSymbol.IsExtensionMethod
? methodSymbol.ReducedFrom?.Parameters.FirstOrDefault()?.Type ?? methodSymbol.Parameters.FirstOrDefault()?.Type
: methodSymbol.ReceiverType;

return receiverType is not null && ImplementsGenericEnumerable(receiverType);
}

// Symbol couldn't be resolved from the invocation. Fall back to the
// receiver's syntactic expression — if its type binds and it isn't a LINQ
// shape, we can still skip the mutation. This catches cases like
// `httpContext.Response.Cookies.Append(...)` where the broader semantic
// resolution may have failed but the receiver's static type is still
// available.
if (node is MemberAccessExpressionSyntax memberAccess)
{
var receiverType = semanticModel.GetTypeInfo(memberAccess.Expression).Type;
if (receiverType is not null && receiverType.TypeKind != TypeKind.Error)
{
return ImplementsGenericEnumerable(receiverType);
}
}

// Receiver type could not be determined — preserve legacy behaviour.
return true;
}

private static bool IsLinqHostType(INamedTypeSymbol type) =>
type.ToDisplayString() is "System.Linq.Enumerable"
or "System.Linq.Queryable"
or "System.Linq.ParallelEnumerable";

private static bool ImplementsGenericEnumerable(ITypeSymbol type)
{
if (IsGenericIEnumerable(type))
{
return true;
}

foreach (var iface in type.AllInterfaces)
{
if (IsGenericIEnumerable(iface))
{
return true;
}
}

return false;
}

private static bool IsGenericIEnumerable(ITypeSymbol type) =>
type.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T;
}
35 changes: 35 additions & 0 deletions src/Stryker.Core/Stryker.Core/Mutators/ObjectCreationMutator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ public override IEnumerable<Mutation> ApplyMutations(ObjectCreationExpressionSyn
}
if (node.Initializer?.Kind() == SyntaxKind.ObjectInitializerExpression && node.Initializer.Expressions.Count > 0)
{
// Skip when the target type has any `required` members — an empty
// initializer would fail to compile with CS9035.
if (HasRequiredMembers(node, semanticModel))
{
yield break;
}
Comment thread
slang25 marked this conversation as resolved.

yield return new Mutation()
{
OriginalNode = node,
Expand All @@ -34,4 +41,32 @@ public override IEnumerable<Mutation> ApplyMutations(ObjectCreationExpressionSyn
};
}
}

private static bool HasRequiredMembers(ObjectCreationExpressionSyntax node, SemanticModel semanticModel)
{
if (semanticModel is null)
{
return false;
}

var typeSymbol = semanticModel.GetTypeInfo(node).Type as INamedTypeSymbol;
if (typeSymbol is null)
{
return false;
}

for (var current = typeSymbol; current is not null; current = current.BaseType)
{
foreach (var member in current.GetMembers())
{
if ((member is IPropertySymbol prop && prop.IsRequired) ||
(member is IFieldSymbol field && field.IsRequired))
{
return true;
}
}
}

return false;
}
}
Loading