Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 7 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,13 @@ 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.
|| x.IsKind(SyntaxKind.SingleVariableDesignation), true);
Comment thread
slang25 marked this conversation as resolved.
Outdated

/// <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 CS8852.
Comment thread
slang25 marked this conversation as resolved.
Outdated
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