From 0925fe905f0c3a431157edd0b5c4c01f3e77b60f Mon Sep 17 00:00:00 2001 From: Pentiva Date: Thu, 31 Oct 2024 16:50:39 +0000 Subject: [PATCH 1/9] feat(Regex): Add mutator for other regex uses --- .../RegexStringSyntaxAttributeMutatorTests.cs | 130 ++++++++++++++++++ .../Mutants/CsharpMutantOrchestrator.cs | 2 + .../ArgumentSpecificOrchestrator.cs | 31 +++++ .../Stryker.Core/Mutators/RegexMutator.cs | 70 +++------- .../Stryker.Core/Mutators/RegexMutatorBase.cs | 73 ++++++++++ .../RegexStringSyntaxAttributeMutator.cs | 43 ++++++ 6 files changed, 297 insertions(+), 52 deletions(-) create mode 100644 src/Stryker.Core/Stryker.Core.UnitTest/Mutators/RegexStringSyntaxAttributeMutatorTests.cs create mode 100644 src/Stryker.Core/Stryker.Core/Mutants/CsharpNodeOrchestrators/ArgumentSpecificOrchestrator.cs create mode 100644 src/Stryker.Core/Stryker.Core/Mutators/RegexMutatorBase.cs create mode 100644 src/Stryker.Core/Stryker.Core/Mutators/RegexStringSyntaxAttributeMutator.cs diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/RegexStringSyntaxAttributeMutatorTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/RegexStringSyntaxAttributeMutatorTests.cs new file mode 100644 index 000000000..bd3908e05 --- /dev/null +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/RegexStringSyntaxAttributeMutatorTests.cs @@ -0,0 +1,130 @@ +using System.IO; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Serilog.Events; +using Stryker.Abstractions; +using Stryker.Abstractions.Mutators; +using Stryker.Abstractions.Options; +using Stryker.Core.InjectedHelpers; +using Stryker.Core.Mutants; + +namespace Stryker.Core.UnitTest.Mutators; + +[TestClass] +public class RegexStringSyntaxAttributeMutatorTests : TestBase +{ + private readonly CodeInjection _injector = new(); + private readonly CsharpMutantOrchestrator _target; + + public RegexStringSyntaxAttributeMutatorTests() => + _target = new CsharpMutantOrchestrator(new MutantPlacer(_injector), + options: new StrykerOptions + { + MutationLevel = MutationLevel.Complete, + OptimizationMode = OptimizationModes.CoverageBasedTest, + ExcludedMutations = [Mutator.Block], + LogOptions = new LogOptions { LogLevel = LogEventLevel.Verbose } + }); + + private async Task ShouldMutateCompiledSourceToExpectedAsync(string actual, string expected) + { + var cSharpParseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview); + var syntaxTree = CSharpSyntaxTree.ParseText(actual, cSharpParseOptions); + + var basePath = Path.GetDirectoryName(typeof(object).Assembly.Location)!; + var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); + var regex = MetadataReference.CreateFromFile(typeof(Regex).Assembly.Location); + var attribute = MetadataReference.CreateFromFile(Path.Combine(basePath, "System.Runtime.dll")); + + Compilation compilation = CSharpCompilation.Create("MyCompilation", + [syntaxTree], [mscorlib, regex, attribute], + new CSharpCompilationOptions(OutputKind + .DynamicallyLinkedLibrary)); + + var actualNode = _target.Mutate(syntaxTree, compilation.GetSemanticModel(syntaxTree)); + actual = (await actualNode.GetRootAsync()).ToFullString(); + actual = actual.Replace(_injector.HelperNamespace, "StrykerNamespace"); + actualNode = CSharpSyntaxTree.ParseText(actual, cSharpParseOptions); + var expectedNode = CSharpSyntaxTree.ParseText(expected); + actualNode.ShouldBeSemantically(expectedNode); + actualNode.ShouldNotContainErrors(); + } + + + [TestMethod] + public Task ShouldMutateRegexStaticMethods() + { + var source = """ + using System.Text.RegularExpressions; + namespace StrykerNet.UnitTest.Mutants.TestResources; + class RegexClass { + bool A(string input) { + return Regex.IsMatch(input, @"^abc"); + } + } + """; + + var expected = """ + using System.Text.RegularExpressions; + namespace StrykerNet.UnitTest.Mutants.TestResources; + class RegexClass { + bool A(string input) { + if (StrykerNamespace.MutantControl.IsActive(0)) { + } else { + return Regex.IsMatch( + input, + (StrykerNamespace.MutantControl.IsActive(1) + ? "abc" + : (StrykerNamespace.MutantControl.IsActive(2) ? "" : @"^abc"))); + } + return default(bool); + } + } + """; + return ShouldMutateCompiledSourceToExpectedAsync(source, expected); + } + + [TestMethod] + public Task ShouldMutateCustomRegexMethods() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + public class C { + public void M() { + Call("^abc"); + } + + public static void Call([StringSyntax(StringSyntaxAttribute.Regex)]string s) { + + } + } + """; + + var expected = + """ + using System.Diagnostics.CodeAnalysis; + public class C { + public void M() { + if (StrykerNamespace.MutantControl.IsActive(0)) { + } else { + if (StrykerNamespace.MutantControl.IsActive(1)) { + ; + } else { + Call( + (StrykerNamespace.MutantControl.IsActive(2) + ? "abc" + : (StrykerNamespace.MutantControl.IsActive(3) ? "" : "^abc"))); + } + } + } + public static void Call( + [StringSyntax(StringSyntaxAttribute.Regex)] string s) {} + } + """; + + return ShouldMutateCompiledSourceToExpectedAsync(source, expected); + } +} diff --git a/src/Stryker.Core/Stryker.Core/Mutants/CsharpMutantOrchestrator.cs b/src/Stryker.Core/Stryker.Core/Mutants/CsharpMutantOrchestrator.cs index 4fb782c00..b475393dd 100644 --- a/src/Stryker.Core/Stryker.Core/Mutants/CsharpMutantOrchestrator.cs +++ b/src/Stryker.Core/Stryker.Core/Mutants/CsharpMutantOrchestrator.cs @@ -88,6 +88,7 @@ private static List BuildOrchestratorList() => new BlockOrchestrator(), new StatementSpecificOrchestrator(), new ExpressionSpecificOrchestrator(), + new ArgumentSpecificOrchestrator(), new SyntaxNodeOrchestrator() ]; @@ -111,6 +112,7 @@ private static List DefaultMutatorList() => new ArrayCreationMutator(), new StatementMutator(), new RegexMutator(), + new RegexStringSyntaxAttributeMutator(), new NullCoalescingExpressionMutator(), new MathMutator(), new SwitchExpressionMutator(), diff --git a/src/Stryker.Core/Stryker.Core/Mutants/CsharpNodeOrchestrators/ArgumentSpecificOrchestrator.cs b/src/Stryker.Core/Stryker.Core/Mutants/CsharpNodeOrchestrators/ArgumentSpecificOrchestrator.cs new file mode 100644 index 000000000..e8d8fc5a8 --- /dev/null +++ b/src/Stryker.Core/Stryker.Core/Mutants/CsharpNodeOrchestrators/ArgumentSpecificOrchestrator.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Stryker.Core.Helpers; + +namespace Stryker.Core.Mutants.CsharpNodeOrchestrators; + +/// +/// Orchestrate mutations for arguments (and sub expressions). +/// +internal class ArgumentSpecificOrchestrator : NodeSpecificOrchestrator +{ + /// + /// Inject all pending mutations controlled with conditional operator(s). + protected override ArgumentSyntax InjectMutations(ArgumentSyntax sourceNode, ArgumentSyntax targetNode, + SemanticModel semanticModel, MutationContext context) => + targetNode.WithExpression(context.InjectMutations(targetNode.Expression, sourceNode.Expression)); + + + protected override MutationContext StoreMutations(ArgumentSyntax node, + IEnumerable mutations, + MutationContext context) => + // if the expression contains a declaration, it must be controlled at the block level. + context.AddMutations(mutations, + node.ContainsDeclarations() ? MutationControl.Block : MutationControl.Expression); + + protected override MutationContext PrepareContext(ArgumentSyntax node, MutationContext context) => + base.PrepareContext(node, context.Enter(MutationControl.Expression)); + + protected override void RestoreContext(MutationContext context) => base.RestoreContext(context.Leave()); +} diff --git a/src/Stryker.Core/Stryker.Core/Mutators/RegexMutator.cs b/src/Stryker.Core/Stryker.Core/Mutators/RegexMutator.cs index 3e33c8feb..44f7d35ed 100644 --- a/src/Stryker.Core/Stryker.Core/Mutators/RegexMutator.cs +++ b/src/Stryker.Core/Stryker.Core/Mutators/RegexMutator.cs @@ -1,72 +1,38 @@ +#nullable enable +using System.Linq; +using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.Extensions.Logging; using Stryker.Abstractions.Logging; -using Stryker.Abstractions.Mutants; using Stryker.Abstractions.Mutators; -using Stryker.RegexMutators; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; namespace Stryker.Core.Mutators; -public class RegexMutator : MutatorBase +public class RegexMutator : RegexMutatorBase { private const string PatternArgumentName = "pattern"; - private ILogger Logger { get; } - public override MutationLevel MutationLevel => MutationLevel.Advanced; + protected override ILogger Logger { get; } = ApplicationLogging.LoggerFactory.CreateLogger(); - public RegexMutator() - { - Logger = ApplicationLogging.LoggerFactory.CreateLogger(); - } + public override MutationLevel MutationLevel => MutationLevel.Advanced; - public override IEnumerable ApplyMutations(ObjectCreationExpressionSyntax node, - SemanticModel semanticModel) + protected override ExpressionSyntax? GetMutateableNode(ObjectCreationExpressionSyntax node, + SemanticModel? semanticModel) { var name = node.Type.ToString(); - if (name == nameof(Regex) || name == typeof(Regex).FullName) + + if (name != nameof(Regex) && name != typeof(Regex).FullName) { - var arguments = node.ArgumentList.Arguments; - var namedArgument = arguments.FirstOrDefault(argument => - argument.NameColon?.Name.Identifier.ValueText == PatternArgumentName); - var patternArgument = namedArgument ?? node.ArgumentList.Arguments.FirstOrDefault(); - var patternExpression = patternArgument?.Expression; + return null; + } - if (patternExpression?.Kind() == SyntaxKind.StringLiteralExpression) - { - var currentValue = ((LiteralExpressionSyntax)patternExpression).Token.ValueText; - var regexMutantOrchestrator = new RegexMutantOrchestrator(currentValue); - var replacementValues = regexMutantOrchestrator.Mutate(); - foreach (var regexMutation in replacementValues) - { - try - { - _ = new Regex(regexMutation.ReplacementPattern); - } - catch (ArgumentException exception) - { - Logger.LogDebug( - "RegexMutator created mutation {CurrentValue} -> {ReplacementPattern} which is an invalid regular expression:\n{Message}", - currentValue, regexMutation.ReplacementPattern, exception.Message); - continue; - } + var arguments = node.ArgumentList?.Arguments; - yield return new Mutation() - { - OriginalNode = patternExpression, - ReplacementNode = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, - SyntaxFactory.Literal(regexMutation.ReplacementPattern)), - DisplayName = regexMutation.DisplayName, - Type = Mutator.Regex, - Description = regexMutation.Description - }; - } - } - } + var namedArgument = arguments?.FirstOrDefault(static argument => + argument.NameColon?.Name.Identifier.ValueText == + PatternArgumentName); + var patternArgument = namedArgument ?? node.ArgumentList?.Arguments.FirstOrDefault(); + return patternArgument?.Expression; } } diff --git a/src/Stryker.Core/Stryker.Core/Mutators/RegexMutatorBase.cs b/src/Stryker.Core/Stryker.Core/Mutators/RegexMutatorBase.cs new file mode 100644 index 000000000..69b83e72a --- /dev/null +++ b/src/Stryker.Core/Stryker.Core/Mutators/RegexMutatorBase.cs @@ -0,0 +1,73 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Extensions.Logging; +using Stryker.Abstractions.Mutants; +using Stryker.Abstractions.Mutators; +using Stryker.RegexMutators; + +namespace Stryker.Core.Mutators; + +public abstract class RegexMutatorBase : MutatorBase where T : SyntaxNode +{ + protected abstract ILogger Logger { get; } + + protected abstract ExpressionSyntax? GetMutateableNode(T node, SemanticModel? semanticModel); + + public sealed override IEnumerable ApplyMutations(T node, SemanticModel? semanticModel) + { + var expression = GetMutateableNode(node, semanticModel); + + if (expression is null) + { + yield break; + } + + foreach (var mutation in GenerateMutations(expression)) + { + yield return mutation; + } + } + + private IEnumerable GenerateMutations(ExpressionSyntax patternExpression) + { + if (patternExpression?.Kind() != SyntaxKind.StringLiteralExpression) + { + yield break; + } + + var currentValue = ((LiteralExpressionSyntax)patternExpression).Token.ValueText; + var regexMutantOrchestrator = new RegexMutantOrchestrator(currentValue); + var replacementValues = regexMutantOrchestrator.Mutate(); + + foreach (var regexMutation in replacementValues) + { + try + { + _ = new Regex(regexMutation.ReplacementPattern); + } + catch (ArgumentException exception) + { + Logger?.LogDebug( + "RegexMutator created mutation {CurrentValue} -> {ReplacementPattern} which is an invalid regular expression:\n{Message}", + currentValue, regexMutation.ReplacementPattern, exception.Message); + continue; + } + + yield return new Mutation + { + OriginalNode = patternExpression, + ReplacementNode = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal(regexMutation + .ReplacementPattern)), + DisplayName = regexMutation.DisplayName, + Type = Mutator.Regex, + Description = regexMutation.Description + }; + } + } +} diff --git a/src/Stryker.Core/Stryker.Core/Mutators/RegexStringSyntaxAttributeMutator.cs b/src/Stryker.Core/Stryker.Core/Mutators/RegexStringSyntaxAttributeMutator.cs new file mode 100644 index 000000000..b7824b14c --- /dev/null +++ b/src/Stryker.Core/Stryker.Core/Mutators/RegexStringSyntaxAttributeMutator.cs @@ -0,0 +1,43 @@ +#nullable enable +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.Extensions.Logging; +using Stryker.Abstractions.Logging; +using Stryker.Abstractions.Mutators; + +namespace Stryker.Core.Mutators; + +public class RegexStringSyntaxAttributeMutator : RegexMutatorBase +{ + protected override ILogger Logger { get; } = + ApplicationLogging.LoggerFactory.CreateLogger(); + + public override MutationLevel MutationLevel => MutationLevel.Complete; + + protected override ExpressionSyntax? GetMutateableNode(ArgumentSyntax node, + SemanticModel? semanticModel) + { + if (semanticModel is null) + { + return null; + } + + var argumentOp = + (semanticModel.GetOperation(node.Parent?.Parent ?? node) as IInvocationOperation)?.Arguments + .SingleOrDefault(a => a.Syntax == node); + + if (argumentOp?.Parameter?.Type.Name is null or not "String") + { + return null; + } + + return argumentOp.Parameter.GetAttributes().Any(IsRegexSyntaxAttribute) ? node.Expression : null; + } + + private static bool IsRegexSyntaxAttribute(AttributeData a) => + (a.AttributeClass?.Name.Equals("StringSyntaxAttribute") ?? false) && + a.ConstructorArguments.FirstOrDefault().Value is StringSyntaxAttribute.Regex; +} From 77d366fee34bc7d89d7658dd363d57e28829ed4d Mon Sep 17 00:00:00 2001 From: Pentiva Date: Thu, 31 Oct 2024 17:18:51 +0000 Subject: [PATCH 2/9] chore: Add change to docs --- docs/regex-mutations.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/regex-mutations.md b/docs/regex-mutations.md index d8c924864..c8f00d345 100644 --- a/docs/regex-mutations.md +++ b/docs/regex-mutations.md @@ -5,6 +5,7 @@ custom_edit_url: https://github.com/stryker-mutator/stryker-net/edit/master/docs --- Stryker supports a variety of regular expression mutators, which are listed below. Do you have a suggestion for a (new) mutator? Feel free to create an [issue](https://github.com/stryker-mutator/stryker-net/issues)! +Regex mutations are applied to arguments that are annotated with `[StringSyntax(StringSyntaxAttribute.Regex)]` on NET7+, and just the Regex constructor on other frameworks. ## Common tokens | Original | Mutated | @@ -160,4 +161,4 @@ Change a normal group to a non-capturing group. | Original | Mutated | |----------|-----------| -| `(abc)` | `(?:abc)` | \ No newline at end of file +| `(abc)` | `(?:abc)` | From 7916864c15be6298907d0dd73dc8079551aa5759 Mon Sep 17 00:00:00 2001 From: Pentiva Date: Thu, 31 Oct 2024 17:19:05 +0000 Subject: [PATCH 3/9] chore: Add more tests --- .../RegexStringSyntaxAttributeMutatorTests.cs | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/RegexStringSyntaxAttributeMutatorTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/RegexStringSyntaxAttributeMutatorTests.cs index bd3908e05..2ad01ee37 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/RegexStringSyntaxAttributeMutatorTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/RegexStringSyntaxAttributeMutatorTests.cs @@ -127,4 +127,72 @@ public static void Call( return ShouldMutateCompiledSourceToExpectedAsync(source, expected); } + + [TestMethod] + public Task ShouldNotMutateCustomNonRegexMethods() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + public class C { + public void M() { + Call("^abc"); + } + + public static void Call(string s) { + + } + } + """; + + var expected = + """ + using System.Diagnostics.CodeAnalysis; + public class C { + public void M() { + if(StrykerNamespace.MutantControl.IsActive(0)){}else{ + if(StrykerNamespace.MutantControl.IsActive(1)){;}else{ + Call((StrykerNamespace.MutantControl.IsActive(2)?"":"^abc")); + } + } + } + public static void Call(string s) {} + } + """; + + return ShouldMutateCompiledSourceToExpectedAsync(source, expected); + } + + [TestMethod] + public Task ShouldNotMutateUnrelatedMethods() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + public class C { + public void M() { + Call(false); + } + + public static void Call(bool s) { + + } + } + """; + + var expected = + """ + using System.Diagnostics.CodeAnalysis; + public class C { + public void M() { + if(StrykerNamespace.MutantControl.IsActive(0)){}else{ + if(StrykerNamespace.MutantControl.IsActive(1)){;}else{ + Call((StrykerNamespace.MutantControl.IsActive(2)?true:false)); + } + } + } + public static void Call(bool s) {} + } + """; + + return ShouldMutateCompiledSourceToExpectedAsync(source, expected); + } } From 5a1e65139cdc7e29130298e323c08f6069fea8e8 Mon Sep 17 00:00:00 2001 From: Pentiva Date: Mon, 4 Nov 2024 22:11:48 +0000 Subject: [PATCH 4/9] feat: Rework String and Regex Mutators into one mutator --- .../Mutators/RegexMutatorTest.cs | 37 +- .../RegexStringSyntaxAttributeMutatorTests.cs | 382 ++++++++++++++++-- .../Mutators/StringMutatorTests.cs | 16 +- .../Mutants/CsharpMutantOrchestrator.cs | 3 - .../ArgumentSpecificOrchestrator.cs | 31 -- .../Stryker.Core/Mutators/RegexMutator.cs | 38 -- .../Stryker.Core/Mutators/RegexMutatorBase.cs | 73 ---- .../RegexStringSyntaxAttributeMutator.cs | 43 -- .../Stryker.Core/Mutators/StringMutator.cs | 208 +++++++++- 9 files changed, 557 insertions(+), 274 deletions(-) delete mode 100644 src/Stryker.Core/Stryker.Core/Mutants/CsharpNodeOrchestrators/ArgumentSpecificOrchestrator.cs delete mode 100644 src/Stryker.Core/Stryker.Core/Mutators/RegexMutator.cs delete mode 100644 src/Stryker.Core/Stryker.Core/Mutators/RegexMutatorBase.cs delete mode 100644 src/Stryker.Core/Stryker.Core/Mutators/RegexStringSyntaxAttributeMutator.cs diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/RegexMutatorTest.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/RegexMutatorTest.cs index a29c88efe..a9357c8b8 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/RegexMutatorTest.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/RegexMutatorTest.cs @@ -14,17 +14,17 @@ public class RegexMutatorTest : TestBase [TestMethod] public void ShouldBeMutationLevelAdvanced() { - var target = new RegexMutator(); - target.MutationLevel.ShouldBe(MutationLevel.Advanced); + var target = new StringMutator(); + target.RegexMutationLevel.ShouldBe(MutationLevel.Advanced); } [TestMethod] public void ShouldMutateStringLiteralInRegexConstructor() { var objectCreationExpression = SyntaxFactory.ParseExpression("new Regex(@\"^abc\")") as ObjectCreationExpressionSyntax; - var target = new RegexMutator(); + var target = new StringMutator(); - var result = target.ApplyMutations(objectCreationExpression, null); + var result = target.ApplyMutations(objectCreationExpression.DescendantNodesAndSelf().OfType().First(), null, MutationLevel.Advanced); var mutation = result.ShouldHaveSingleItem(); @@ -37,9 +37,9 @@ public void ShouldMutateStringLiteralInRegexConstructor() public void ShouldMutateStringLiteralInRegexConstructorWithFullName() { var objectCreationExpression = SyntaxFactory.ParseExpression("new System.Text.RegularExpressions.Regex(@\"^abc\")") as ObjectCreationExpressionSyntax; - var target = new RegexMutator(); + var target = new StringMutator(); - var result = target.ApplyMutations(objectCreationExpression, null); + var result = target.ApplyMutations(objectCreationExpression.DescendantNodesAndSelf().OfType().First(), null, MutationLevel.Advanced); var mutation = result.ShouldHaveSingleItem(); @@ -48,34 +48,23 @@ public void ShouldMutateStringLiteralInRegexConstructorWithFullName() replacement.Token.ValueText.ShouldBe("abc"); } - - [TestMethod] - public void ShouldNotMutateRegexWithoutParameters() - { - var objectCreationExpression = SyntaxFactory.ParseExpression("new Regex()") as ObjectCreationExpressionSyntax; - var target = new RegexMutator(); - var result = target.ApplyMutations(objectCreationExpression, null); - - result.ShouldBeEmpty(); - } - [TestMethod] public void ShouldNotMutateStringLiteralInOtherConstructor() { var objectCreationExpression = SyntaxFactory.ParseExpression("new Other(@\"^abc\")") as ObjectCreationExpressionSyntax; - var target = new RegexMutator(); - var result = target.ApplyMutations(objectCreationExpression, null); + var target = new StringMutator(); + var result = target.ApplyMutations(objectCreationExpression.DescendantNodesAndSelf().OfType().First(), null, MutationLevel.Advanced); - result.ShouldBeEmpty(); + result.Where(a=>a.Type == Mutator.Regex).ShouldBeEmpty(); } [TestMethod] public void ShouldMutateStringLiteralMultipleTimes() { var objectCreationExpression = SyntaxFactory.ParseExpression("new Regex(@\"^abc$\")") as ObjectCreationExpressionSyntax; - var target = new RegexMutator(); + var target = new StringMutator(); - var result = target.ApplyMutations(objectCreationExpression, null); + var result = target.ApplyMutations(objectCreationExpression.DescendantNodesAndSelf().OfType().First(), null, MutationLevel.Advanced); result.Count().ShouldBe(2); result.ShouldAllBe(mutant => mutant.DisplayName == "Regex anchor removal mutation"); @@ -89,9 +78,9 @@ public void ShouldMutateStringLiteralMultipleTimes() public void ShouldMutateStringLiteralAsNamedArgumentPatternInRegexConstructor() { var objectCreationExpression = SyntaxFactory.ParseExpression("new Regex(options: RegexOptions.None, pattern: @\"^abc\")") as ObjectCreationExpressionSyntax; - var target = new RegexMutator(); + var target = new StringMutator(); - var result = target.ApplyMutations(objectCreationExpression, null); + var result = target.ApplyMutations(objectCreationExpression.DescendantNodesAndSelf().OfType().First(), null, MutationLevel.Advanced); var mutation = result.ShouldHaveSingleItem(); diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/RegexStringSyntaxAttributeMutatorTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/RegexStringSyntaxAttributeMutatorTests.cs index 2ad01ee37..1ccc1fef4 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/RegexStringSyntaxAttributeMutatorTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/RegexStringSyntaxAttributeMutatorTests.cs @@ -25,7 +25,7 @@ public RegexStringSyntaxAttributeMutatorTests() => { MutationLevel = MutationLevel.Complete, OptimizationMode = OptimizationModes.CoverageBasedTest, - ExcludedMutations = [Mutator.Block], + ExcludedMutations = [Mutator.Block, Mutator.Statement], LogOptions = new LogOptions { LogLevel = LogEventLevel.Verbose } }); @@ -58,6 +58,7 @@ private async Task ShouldMutateCompiledSourceToExpectedAsync(string actual, stri public Task ShouldMutateRegexStaticMethods() { var source = """ + // Stryker disable block,statement using System.Text.RegularExpressions; namespace StrykerNet.UnitTest.Mutants.TestResources; class RegexClass { @@ -68,20 +69,13 @@ bool A(string input) { """; var expected = """ + // Stryker disable block,statement using System.Text.RegularExpressions; namespace StrykerNet.UnitTest.Mutants.TestResources; class RegexClass { - bool A(string input) { - if (StrykerNamespace.MutantControl.IsActive(0)) { - } else { - return Regex.IsMatch( - input, - (StrykerNamespace.MutantControl.IsActive(1) - ? "abc" - : (StrykerNamespace.MutantControl.IsActive(2) ? "" : @"^abc"))); + bool A(string input) { + return Regex.IsMatch(input, (StrykerNamespace.MutantControl.IsActive(1)?"abc":@"^abc")); } - return default(bool); - } } """; return ShouldMutateCompiledSourceToExpectedAsync(source, expected); @@ -91,6 +85,7 @@ bool A(string input) { public Task ShouldMutateCustomRegexMethods() { var source = """ + // Stryker disable block,statement using System.Diagnostics.CodeAnalysis; public class C { public void M() { @@ -105,23 +100,15 @@ public static void Call([StringSyntax(StringSyntaxAttribute.Regex)]string s) { var expected = """ + // Stryker disable block,statement using System.Diagnostics.CodeAnalysis; public class C { - public void M() { - if (StrykerNamespace.MutantControl.IsActive(0)) { - } else { - if (StrykerNamespace.MutantControl.IsActive(1)) { - ; - } else { - Call( - (StrykerNamespace.MutantControl.IsActive(2) - ? "abc" - : (StrykerNamespace.MutantControl.IsActive(3) ? "" : "^abc"))); - } + public void M() { + Call((StrykerNamespace.MutantControl.IsActive(2)?"abc":"^abc")); + } + public static void Call([StringSyntax(StringSyntaxAttribute.Regex)]string s) { + } - } - public static void Call( - [StringSyntax(StringSyntaxAttribute.Regex)] string s) {} } """; @@ -132,28 +119,26 @@ public static void Call( public Task ShouldNotMutateCustomNonRegexMethods() { var source = """ + // Stryker disable block,statement using System.Diagnostics.CodeAnalysis; public class C { public void M() { Call("^abc"); } - + public static void Call(string s) { - + } } """; var expected = """ + // Stryker disable block,statement using System.Diagnostics.CodeAnalysis; public class C { public void M() { - if(StrykerNamespace.MutantControl.IsActive(0)){}else{ - if(StrykerNamespace.MutantControl.IsActive(1)){;}else{ - Call((StrykerNamespace.MutantControl.IsActive(2)?"":"^abc")); - } - } + Call((StrykerNamespace.MutantControl.IsActive(2)?"":"^abc")); } public static void Call(string s) {} } @@ -166,28 +151,26 @@ public static void Call(string s) {} public Task ShouldNotMutateUnrelatedMethods() { var source = """ + // Stryker disable block,statement using System.Diagnostics.CodeAnalysis; public class C { public void M() { Call(false); } - + public static void Call(bool s) { - + } } """; var expected = """ + // Stryker disable block,statement using System.Diagnostics.CodeAnalysis; public class C { public void M() { - if(StrykerNamespace.MutantControl.IsActive(0)){}else{ - if(StrykerNamespace.MutantControl.IsActive(1)){;}else{ - Call((StrykerNamespace.MutantControl.IsActive(2)?true:false)); - } - } + Call((StrykerNamespace.MutantControl.IsActive(2)?true:false)); } public static void Call(bool s) {} } @@ -195,4 +178,325 @@ public static void Call(bool s) {} return ShouldMutateCompiledSourceToExpectedAsync(source, expected); } + + [TestMethod] + public Task ShouldMutateRegexFields() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + public class C { + [StringSyntax(StringSyntaxAttribute.Regex)] + public string RegexPattern = "^abc"; + } + """; + + var expected = + """ + using System.Diagnostics.CodeAnalysis; + public class C { + [StringSyntax(StringSyntaxAttribute.Regex)] + public string RegexPattern = (StrykerNamespace.MutantControl.IsActive(0)?"abc":"^abc"); + } + """; + + return ShouldMutateCompiledSourceToExpectedAsync(source, expected); + } + + [TestMethod] + public Task ShouldMutateRegexProperties() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + public class C { + [StringSyntax(StringSyntaxAttribute.Regex)] + public string RegexPattern => "^abc"; + } + """; + + var expected = + """ + using System.Diagnostics.CodeAnalysis; + public class C { + [StringSyntax(StringSyntaxAttribute.Regex)] + public string RegexPattern => (StrykerNamespace.MutantControl.IsActive(0)?"abc":"^abc"); + } + """; + + return ShouldMutateCompiledSourceToExpectedAsync(source, expected); + } + + [TestMethod] + public Task ShouldNotApplyRegexMutationToNormalFields() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + public class C { + public string RegexPattern = "^abc"; + } + """; + + var expected = + """ + using System.Diagnostics.CodeAnalysis; + public class C { + public string RegexPattern = (StrykerNamespace.MutantControl.IsActive(0)?"":"^abc"); + } + """; + + return ShouldMutateCompiledSourceToExpectedAsync(source, expected); + } + + [TestMethod] + public Task ShouldNotMutateOtherFields() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + public class C { + public bool RegexPattern = true; + } + """; + + var expected = + """ + using System.Diagnostics.CodeAnalysis; + public class C { + public bool RegexPattern = (StrykerNamespace.MutantControl.IsActive(0)?false:true); + } + """; + + return ShouldMutateCompiledSourceToExpectedAsync(source, expected); + } + + [TestMethod] + public Task ShouldMutateImplicitRegexConstructor() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + public class C { + public static Regex RegexPattern = new("^abc"); + } + """; + + var expected = + """ + using System.Diagnostics.CodeAnalysis; + public class C { + public static Regex RegexPattern = StrykerNamespace.MutantContext.TrackValue(()=>new((StrykerNamespace.MutantControl.IsActive(0)?"abc":"^abc"))); + } + """; + + return ShouldMutateCompiledSourceToExpectedAsync(source, expected); + } + + [TestMethod] + public Task ShouldMutateRegexFieldAssignment() + { + var source = """ + // Stryker disable block,statement + using System.Diagnostics.CodeAnalysis; + public class C { + [StringSyntax(StringSyntaxAttribute.Regex)] + public string RegexPattern; + + public void M() { + RegexPattern = "^abc"; + } + } + """; + + var expected = + """ + // Stryker disable block,statement + using System.Diagnostics.CodeAnalysis; + public class C { + [StringSyntax(StringSyntaxAttribute.Regex)] + public string RegexPattern; + + public void M() { + RegexPattern = (StrykerNamespace.MutantControl.IsActive(1)?"abc":"^abc"); + } + } + """; + + return ShouldMutateCompiledSourceToExpectedAsync(source, expected); + } + + [TestMethod] + public Task ShouldMutateRegexPropertyAssignment() + { + var source = """ + // Stryker disable block,statement + using System.Diagnostics.CodeAnalysis; + public class C { + [StringSyntax(StringSyntaxAttribute.Regex)] + public string RegexPattern { get; set; } + + public void M() { + RegexPattern = "^abc"; + } + } + """; + + var expected = + """ + // Stryker disable block,statement + using System.Diagnostics.CodeAnalysis; + public class C { + [StringSyntax(StringSyntaxAttribute.Regex)] + public string RegexPattern { get; set; } + + public void M() { + RegexPattern = (StrykerNamespace.MutantControl.IsActive(1)?"abc":"^abc"); + } + } + """; + + return ShouldMutateCompiledSourceToExpectedAsync(source, expected); + } + + [TestMethod] + public Task ShouldNotMutateNonRegexFieldAssignment() + { + var source = """ + // Stryker disable block,statement + using System.Diagnostics.CodeAnalysis; + public class C { + public string RegexPattern; + + public void M() { + RegexPattern = "^abc"; + } + } + """; + + var expected = + """ + // Stryker disable block,statement + using System.Diagnostics.CodeAnalysis; + public class C { + public string RegexPattern; + + public void M() { + RegexPattern = (StrykerNamespace.MutantControl.IsActive(1)?"":"^abc"); + } + } + """; + + return ShouldMutateCompiledSourceToExpectedAsync(source, expected); + } + + [TestMethod] + public Task ShouldNotMutateNonRegexPropertyAssignment() + { + var source = """ + // Stryker disable block,statement + using System.Diagnostics.CodeAnalysis; + public class C { + public string RegexPattern { get; set; } + + public void M() { + RegexPattern = "^abc"; + } + } + """; + + var expected = + """ + // Stryker disable block,statement + using System.Diagnostics.CodeAnalysis; + public class C { + public string RegexPattern { get; set; } + + public void M() { + RegexPattern = (StrykerNamespace.MutantControl.IsActive(1)?"":"^abc"); + } + } + """; + + return ShouldMutateCompiledSourceToExpectedAsync(source, expected); + } + + [TestMethod] + public Task ShouldMutateFullyQualifiedAttributeCustomRegexMethod() + { + var source = """ + // Stryker disable block,statement + public class C { + public void M() { + Call("^abc"); + } + + public static void Call([System.Diagnostics.CodeAnalysis.StringSyntax(System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.Regex)]string s) { + + } + } + """; + + var expected = + """ + // Stryker disable block,statement + public class C { + public void M() { + Call((StrykerNamespace.MutantControl.IsActive(2)?"abc":"^abc")); + } + public static void Call([System.Diagnostics.CodeAnalysis.StringSyntax(System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.Regex)]string s) { + + } + } + """; + + return ShouldMutateCompiledSourceToExpectedAsync(source, expected); + } + + [TestMethod] + public Task ShouldMutateFullyQualifiedAttributeOnFieldAssignment() + { + var source = """ + // Stryker disable block,statement + public class C { + [System.Diagnostics.CodeAnalysis.StringSyntax(System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.Regex)] + public string RegexPattern; + + public void M() { + RegexPattern = "^abc"; + } + } + """; + + var expected = + """ + // Stryker disable block,statement + public class C { + [System.Diagnostics.CodeAnalysis.StringSyntax(System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.Regex)] + public string RegexPattern; + + public void M() { + RegexPattern = (StrykerNamespace.MutantControl.IsActive(1)?"abc":"^abc"); + } + } + """; + + return ShouldMutateCompiledSourceToExpectedAsync(source, expected); + } + + [TestMethod] + public Task ShouldMutateFullyQualifiedAttributeOnFieldInitialisation() + { + var source = """ + public class C { + [System.Diagnostics.CodeAnalysis.StringSyntax(System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.Regex)] + public string RegexPattern = "^abc"; + } + """; + + var expected = + """ + public class C { + [System.Diagnostics.CodeAnalysis.StringSyntax(System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.Regex)] + public string RegexPattern = (StrykerNamespace.MutantControl.IsActive(0)?"abc":"^abc"); + } + """; + + return ShouldMutateCompiledSourceToExpectedAsync(source, expected); + } } diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/StringMutatorTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/StringMutatorTests.cs index 73d827274..31bcda68d 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/StringMutatorTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/StringMutatorTests.cs @@ -15,7 +15,7 @@ public class StringMutatorTests : TestBase public void ShouldBeMutationLevelStandard() { var target = new StringMutator(); - target.MutationLevel.ShouldBe(MutationLevel.Standard); + target.OtherMutationLevel.ShouldBe(MutationLevel.Standard); } [TestMethod] @@ -26,7 +26,7 @@ public void ShouldMutate(string original, string expected) var node = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(original)); var mutator = new StringMutator(); - var result = mutator.ApplyMutations(node, null).ToList(); + var result = mutator.ApplyMutations(node, null, MutationLevel.Standard).ToList(); var mutation = result.ShouldHaveSingleItem(); @@ -41,7 +41,7 @@ public void ShouldNotMutateOnRegexExpression() var expressionSyntax = SyntaxFactory.ParseExpression("new Regex(\"myregex\")"); var literalExpression = expressionSyntax.DescendantNodes().OfType().First(); var mutator = new StringMutator(); - var result = mutator.ApplyMutations(literalExpression, null).ToList(); + var result = mutator.ApplyMutations(literalExpression, null, MutationLevel.Standard).ToList(); result.ShouldBeEmpty(); } @@ -52,7 +52,7 @@ public void ShouldNotMutateOnFullyDefinedRegexExpression() var expressionSyntax = SyntaxFactory.ParseExpression("new System.Text.RegularExpressions.Regex(\"myregex\")"); var literalExpression = expressionSyntax.DescendantNodes().OfType().First(); var mutator = new StringMutator(); - var result = mutator.ApplyMutations(literalExpression, null).ToList(); + var result = mutator.ApplyMutations(literalExpression, null, MutationLevel.Standard).ToList(); result.ShouldBeEmpty(); } @@ -72,7 +72,7 @@ public Regex GetRegex(){ "); var literalExpression = syntaxTree.GetRoot().DescendantNodes().OfType().First(); var mutator = new StringMutator(); - var result = mutator.ApplyMutations(literalExpression, null).ToList(); + var result = mutator.ApplyMutations(literalExpression, null, MutationLevel.Standard).ToList(); result.ShouldBeEmpty(); } @@ -83,7 +83,7 @@ public void ShouldNotMutateOnGuidExpression() var expressionSyntax = SyntaxFactory.ParseExpression("new Guid(\"00000-0000\")"); var literalExpression = expressionSyntax.DescendantNodes().OfType().First(); var mutator = new StringMutator(); - var result = mutator.ApplyMutations(literalExpression, null).ToList(); + var result = mutator.ApplyMutations(literalExpression, null, MutationLevel.Standard).ToList(); result.ShouldBeEmpty(); } @@ -94,7 +94,7 @@ public void ShouldNotMutateOnFullyDefinedGuidExpression() var expressionSyntax = SyntaxFactory.ParseExpression("new System.Guid(\"00000-0000\")"); var literalExpression = expressionSyntax.DescendantNodes().OfType().First(); var mutator = new StringMutator(); - var result = mutator.ApplyMutations(literalExpression, null).ToList(); + var result = mutator.ApplyMutations(literalExpression, null, MutationLevel.Standard).ToList(); result.ShouldBeEmpty(); } @@ -114,7 +114,7 @@ public Guid GetGuid(){ "); var literalExpression = syntaxTree.GetRoot().DescendantNodes().OfType().First(); var mutator = new StringMutator(); - var result = mutator.ApplyMutations(literalExpression, null).ToList(); + var result = mutator.ApplyMutations(literalExpression, null, MutationLevel.Standard).ToList(); result.ShouldBeEmpty(); } diff --git a/src/Stryker.Core/Stryker.Core/Mutants/CsharpMutantOrchestrator.cs b/src/Stryker.Core/Stryker.Core/Mutants/CsharpMutantOrchestrator.cs index b475393dd..405ed9238 100644 --- a/src/Stryker.Core/Stryker.Core/Mutants/CsharpMutantOrchestrator.cs +++ b/src/Stryker.Core/Stryker.Core/Mutants/CsharpMutantOrchestrator.cs @@ -88,7 +88,6 @@ private static List BuildOrchestratorList() => new BlockOrchestrator(), new StatementSpecificOrchestrator(), new ExpressionSpecificOrchestrator(), - new ArgumentSpecificOrchestrator(), new SyntaxNodeOrchestrator() ]; @@ -111,8 +110,6 @@ private static List DefaultMutatorList() => new ObjectCreationMutator(), new ArrayCreationMutator(), new StatementMutator(), - new RegexMutator(), - new RegexStringSyntaxAttributeMutator(), new NullCoalescingExpressionMutator(), new MathMutator(), new SwitchExpressionMutator(), diff --git a/src/Stryker.Core/Stryker.Core/Mutants/CsharpNodeOrchestrators/ArgumentSpecificOrchestrator.cs b/src/Stryker.Core/Stryker.Core/Mutants/CsharpNodeOrchestrators/ArgumentSpecificOrchestrator.cs deleted file mode 100644 index e8d8fc5a8..000000000 --- a/src/Stryker.Core/Stryker.Core/Mutants/CsharpNodeOrchestrators/ArgumentSpecificOrchestrator.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Stryker.Core.Helpers; - -namespace Stryker.Core.Mutants.CsharpNodeOrchestrators; - -/// -/// Orchestrate mutations for arguments (and sub expressions). -/// -internal class ArgumentSpecificOrchestrator : NodeSpecificOrchestrator -{ - /// - /// Inject all pending mutations controlled with conditional operator(s). - protected override ArgumentSyntax InjectMutations(ArgumentSyntax sourceNode, ArgumentSyntax targetNode, - SemanticModel semanticModel, MutationContext context) => - targetNode.WithExpression(context.InjectMutations(targetNode.Expression, sourceNode.Expression)); - - - protected override MutationContext StoreMutations(ArgumentSyntax node, - IEnumerable mutations, - MutationContext context) => - // if the expression contains a declaration, it must be controlled at the block level. - context.AddMutations(mutations, - node.ContainsDeclarations() ? MutationControl.Block : MutationControl.Expression); - - protected override MutationContext PrepareContext(ArgumentSyntax node, MutationContext context) => - base.PrepareContext(node, context.Enter(MutationControl.Expression)); - - protected override void RestoreContext(MutationContext context) => base.RestoreContext(context.Leave()); -} diff --git a/src/Stryker.Core/Stryker.Core/Mutators/RegexMutator.cs b/src/Stryker.Core/Stryker.Core/Mutators/RegexMutator.cs deleted file mode 100644 index 44f7d35ed..000000000 --- a/src/Stryker.Core/Stryker.Core/Mutators/RegexMutator.cs +++ /dev/null @@ -1,38 +0,0 @@ -#nullable enable -using System.Linq; -using System.Text.RegularExpressions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.Extensions.Logging; -using Stryker.Abstractions.Logging; -using Stryker.Abstractions.Mutators; - -namespace Stryker.Core.Mutators; - -public class RegexMutator : RegexMutatorBase -{ - private const string PatternArgumentName = "pattern"; - - protected override ILogger Logger { get; } = ApplicationLogging.LoggerFactory.CreateLogger(); - - public override MutationLevel MutationLevel => MutationLevel.Advanced; - - protected override ExpressionSyntax? GetMutateableNode(ObjectCreationExpressionSyntax node, - SemanticModel? semanticModel) - { - var name = node.Type.ToString(); - - if (name != nameof(Regex) && name != typeof(Regex).FullName) - { - return null; - } - - var arguments = node.ArgumentList?.Arguments; - - var namedArgument = arguments?.FirstOrDefault(static argument => - argument.NameColon?.Name.Identifier.ValueText == - PatternArgumentName); - var patternArgument = namedArgument ?? node.ArgumentList?.Arguments.FirstOrDefault(); - return patternArgument?.Expression; - } -} diff --git a/src/Stryker.Core/Stryker.Core/Mutators/RegexMutatorBase.cs b/src/Stryker.Core/Stryker.Core/Mutators/RegexMutatorBase.cs deleted file mode 100644 index 69b83e72a..000000000 --- a/src/Stryker.Core/Stryker.Core/Mutators/RegexMutatorBase.cs +++ /dev/null @@ -1,73 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.Extensions.Logging; -using Stryker.Abstractions.Mutants; -using Stryker.Abstractions.Mutators; -using Stryker.RegexMutators; - -namespace Stryker.Core.Mutators; - -public abstract class RegexMutatorBase : MutatorBase where T : SyntaxNode -{ - protected abstract ILogger Logger { get; } - - protected abstract ExpressionSyntax? GetMutateableNode(T node, SemanticModel? semanticModel); - - public sealed override IEnumerable ApplyMutations(T node, SemanticModel? semanticModel) - { - var expression = GetMutateableNode(node, semanticModel); - - if (expression is null) - { - yield break; - } - - foreach (var mutation in GenerateMutations(expression)) - { - yield return mutation; - } - } - - private IEnumerable GenerateMutations(ExpressionSyntax patternExpression) - { - if (patternExpression?.Kind() != SyntaxKind.StringLiteralExpression) - { - yield break; - } - - var currentValue = ((LiteralExpressionSyntax)patternExpression).Token.ValueText; - var regexMutantOrchestrator = new RegexMutantOrchestrator(currentValue); - var replacementValues = regexMutantOrchestrator.Mutate(); - - foreach (var regexMutation in replacementValues) - { - try - { - _ = new Regex(regexMutation.ReplacementPattern); - } - catch (ArgumentException exception) - { - Logger?.LogDebug( - "RegexMutator created mutation {CurrentValue} -> {ReplacementPattern} which is an invalid regular expression:\n{Message}", - currentValue, regexMutation.ReplacementPattern, exception.Message); - continue; - } - - yield return new Mutation - { - OriginalNode = patternExpression, - ReplacementNode = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, - SyntaxFactory.Literal(regexMutation - .ReplacementPattern)), - DisplayName = regexMutation.DisplayName, - Type = Mutator.Regex, - Description = regexMutation.Description - }; - } - } -} diff --git a/src/Stryker.Core/Stryker.Core/Mutators/RegexStringSyntaxAttributeMutator.cs b/src/Stryker.Core/Stryker.Core/Mutators/RegexStringSyntaxAttributeMutator.cs deleted file mode 100644 index b7824b14c..000000000 --- a/src/Stryker.Core/Stryker.Core/Mutators/RegexStringSyntaxAttributeMutator.cs +++ /dev/null @@ -1,43 +0,0 @@ -#nullable enable -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Operations; -using Microsoft.Extensions.Logging; -using Stryker.Abstractions.Logging; -using Stryker.Abstractions.Mutators; - -namespace Stryker.Core.Mutators; - -public class RegexStringSyntaxAttributeMutator : RegexMutatorBase -{ - protected override ILogger Logger { get; } = - ApplicationLogging.LoggerFactory.CreateLogger(); - - public override MutationLevel MutationLevel => MutationLevel.Complete; - - protected override ExpressionSyntax? GetMutateableNode(ArgumentSyntax node, - SemanticModel? semanticModel) - { - if (semanticModel is null) - { - return null; - } - - var argumentOp = - (semanticModel.GetOperation(node.Parent?.Parent ?? node) as IInvocationOperation)?.Arguments - .SingleOrDefault(a => a.Syntax == node); - - if (argumentOp?.Parameter?.Type.Name is null or not "String") - { - return null; - } - - return argumentOp.Parameter.GetAttributes().Any(IsRegexSyntaxAttribute) ? node.Expression : null; - } - - private static bool IsRegexSyntaxAttribute(AttributeData a) => - (a.AttributeClass?.Name.Equals("StringSyntaxAttribute") ?? false) && - a.ConstructorArguments.FirstOrDefault().Value is StringSyntaxAttribute.Regex; -} diff --git a/src/Stryker.Core/Stryker.Core/Mutators/StringMutator.cs b/src/Stryker.Core/Stryker.Core/Mutators/StringMutator.cs index 95512fe42..0694c61f3 100644 --- a/src/Stryker.Core/Stryker.Core/Mutators/StringMutator.cs +++ b/src/Stryker.Core/Stryker.Core/Mutators/StringMutator.cs @@ -1,46 +1,216 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.Extensions.Logging; +using Stryker.Abstractions.Logging; using Stryker.Abstractions.Mutants; using Stryker.Abstractions.Mutators; -using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; +using Stryker.Abstractions.Options; +using Stryker.RegexMutators; namespace Stryker.Core.Mutators; -public class StringMutator : MutatorBase +public class StringMutator : IMutator { - public override MutationLevel MutationLevel => MutationLevel.Standard; + private ILogger Logger { get; } = ApplicationLogging.LoggerFactory.CreateLogger(); - public override IEnumerable ApplyMutations(LiteralExpressionSyntax node, SemanticModel semanticModel) + public MutationLevel RegexMutationLevel => MutationLevel.Advanced; + + public MutationLevel OtherMutationLevel => MutationLevel.Standard; + + public IEnumerable Mutate(SyntaxNode node, SemanticModel semanticModel, IStrykerOptions options) { - // Get objectCreationSyntax to check if it contains a regex type. - var root = node.Parent?.Parent?.Parent; + if (node is LiteralExpressionSyntax tNode && node.IsKind(SyntaxKind.StringLiteralExpression)) + { + // the node was of the expected type, so invoke the mutation method + return ApplyMutations(tNode, semanticModel, options.MutationLevel); + } - if (!IsSpecialType(root) && IsStringLiteral(node)) + return []; + } + + public IEnumerable ApplyMutations(LiteralExpressionSyntax node, SemanticModel semanticModel, + MutationLevel mutationLevel) + { + if (IsRegexString(node, semanticModel)) + { + if (RegexMutationLevel > mutationLevel) + { + yield break; + } + + var currentValue = node.Token.ValueText; + var regexMutantOrchestrator = new RegexMutantOrchestrator(currentValue); + var replacementValues = regexMutantOrchestrator.Mutate(); + + foreach (var regexMutation in replacementValues) + { + try + { + _ = new Regex(regexMutation.ReplacementPattern); + } + catch (ArgumentException exception) + { + Logger?.LogDebug( + "RegexMutator created mutation {CurrentValue} -> {ReplacementPattern} which is an invalid regular expression:\n{Message}", + currentValue, regexMutation.ReplacementPattern, exception.Message); + continue; + } + + yield return new Mutation + { + OriginalNode = node, + ReplacementNode = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal(regexMutation + .ReplacementPattern)), + DisplayName = regexMutation.DisplayName, + Type = Mutator.Regex, + Description = regexMutation.Description + }; + } + } + else if (OtherMutationLevel <= mutationLevel && !IsSpecialType(node.Parent?.Parent?.Parent)) { var currentValue = (string)node.Token.Value; var replacementValue = currentValue == "" ? "Stryker was here!" : ""; + yield return new Mutation { OriginalNode = node, - ReplacementNode = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(replacementValue)), + ReplacementNode = + SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal(replacementValue)), DisplayName = "String mutation", - Type = Mutator.String + Type = Mutator.String }; } } - private static bool IsStringLiteral(LiteralExpressionSyntax node) + private static bool IsRegexString(SyntaxNode node, SemanticModel semanticModel) { - var kind = node.Kind(); - return kind == SyntaxKind.StringLiteralExpression; + foreach (var syntaxNode in node.AncestorsAndSelf()) + { + switch (syntaxNode) + { + case ObjectCreationExpressionSyntax ctor: + return IsCtorOfType(ctor, typeof(Regex)); + case ImplicitObjectCreationExpressionSyntax ctor: + return IsCtorOfType(ctor, typeof(Regex), semanticModel); + case ArgumentSyntax argument when semanticModel is not null && + argument.Parent?.Parent is InvocationExpressionSyntax parentInvocation + : + var invocationOp = semanticModel?.GetOperation(parentInvocation) as IInvocationOperation; + var argumentOp = invocationOp?.Arguments.SingleOrDefault(a => a.Syntax == argument); + + return argumentOp?.Parameter?.Type.Name is "String" && + argumentOp.Parameter.GetAttributes().Any(IsRegexSyntaxAttribute); + case FieldDeclarationSyntax field: + return field.AttributeLists.Any(static a => a.Attributes.Any(IsRegexSyntaxAttribute)); + case PropertyDeclarationSyntax field: + return field.AttributeLists.Any(static a => a.Attributes.Any(IsRegexSyntaxAttribute)); + case AssignmentExpressionSyntax assignment + when semanticModel?.GetOperation(assignment) is ISimpleAssignmentOperation + { + Type.SpecialType: SpecialType.System_String + } expressionOp: + return expressionOp?.Target switch + { + IFieldReferenceOperation field => field.Field.GetAttributes().Any(IsRegexSyntaxAttribute), + IPropertyReferenceOperation property => property.Property.GetAttributes() + .Any(IsRegexSyntaxAttribute), + _ => false + }; + } + } + + return false; } + private static bool IsRegexSyntaxAttribute(AttributeSyntax attributeSyntax) => + IsStringSyntaxAttribute(attributeSyntax.Name) && + attributeSyntax.ArgumentList?.Arguments is [var arg] && + (arg.Expression is + LiteralExpressionSyntax { Token.Text: "Regex" } or + IdentifierNameSyntax { Identifier.Text: "Regex" } || + (arg.Expression is MemberAccessExpressionSyntax + { + Expression: var e, + Name: IdentifierNameSyntax { Identifier.Text: "Regex" } + } && IsStringSyntaxAttribute(e))); + + private static bool IsStringSyntaxAttribute(ExpressionSyntax attributeSyntax) => + attributeSyntax is IdentifierNameSyntax + { + Identifier.Text: "StringSyntax" or "StringSyntaxAttribute" + } or MemberAccessExpressionSyntax + { + Expression: MemberAccessExpressionSyntax + { + Expression: MemberAccessExpressionSyntax + { + Expression: IdentifierNameSyntax + { + Identifier.Text: "System" + }, + Name: IdentifierNameSyntax + { + Identifier.Text: "Diagnostics" + } + }, + Name: IdentifierNameSyntax + { + Identifier.Text: "CodeAnalysis" + } + }, + Name: IdentifierNameSyntax + { + Identifier.Text: "StringSyntax" or "StringSyntaxAttribute" + } + }; + + private static bool IsStringSyntaxAttribute(NameSyntax attributeSyntax) => + attributeSyntax is IdentifierNameSyntax + { + Identifier.Text: "StringSyntax" or "StringSyntaxAttribute" + } or QualifiedNameSyntax + { + Left: QualifiedNameSyntax + { + Left: QualifiedNameSyntax + { + Left: IdentifierNameSyntax + { + Identifier.Text: "System" + }, + Right: IdentifierNameSyntax + { + Identifier.Text: "Diagnostics" + } + }, + Right: IdentifierNameSyntax + { + Identifier.Text: "CodeAnalysis" + } + }, + Right: IdentifierNameSyntax + { + Identifier.Text: "StringSyntax" or "StringSyntaxAttribute" + } + }; + + private static bool IsRegexSyntaxAttribute(AttributeData attributeData) => + (attributeData.AttributeClass?.Name.Equals("StringSyntaxAttribute") ?? false) && + attributeData.ConstructorArguments.FirstOrDefault().Value is StringSyntaxAttribute.Regex; + private static bool IsSpecialType(SyntaxNode root) => root switch { - ObjectCreationExpressionSyntax ctor => IsCtorOfType(ctor, typeof(Regex)) || IsCtorOfType(ctor, typeof(Guid)), + ObjectCreationExpressionSyntax ctor => IsCtorOfType(ctor, typeof(Guid)), _ => false }; @@ -49,4 +219,12 @@ private static bool IsCtorOfType(ObjectCreationExpressionSyntax ctor, Type type) var ctorType = ctor.Type.ToString(); return ctorType == type.Name || ctorType == type.FullName; } + + private static bool IsCtorOfType(ImplicitObjectCreationExpressionSyntax ctor, Type type, + SemanticModel semanticModel) + { + var ti = semanticModel?.GetTypeInfo(ctor); + var ctorType = ti?.Type?.ToDisplayString(); + return ctorType == type.Name || ctorType == type.FullName; + } } From a980c239bb87c7f1a44f929bf5a6a9b3ea442d77 Mon Sep 17 00:00:00 2001 From: Pentiva Date: Tue, 5 Nov 2024 12:07:57 +0000 Subject: [PATCH 5/9] Simplify New Tests Group old and new string tests. --- .../RegexStringSyntaxAttributeMutatorTests.cs | 502 ------------------ .../{ => Strings}/RegexMutatorTest.cs | 41 +- .../RegexMutatorWithSemanticModelTests.cs | 382 +++++++++++++ .../{ => Strings}/StringEmptyMutatorTests.cs | 2 +- .../{ => Strings}/StringMethodMutatorTests.cs | 2 +- .../{ => Strings}/StringMutatorTests.cs | 2 +- .../Stryker.Core/Mutators/StringMutator.cs | 105 ++-- 7 files changed, 472 insertions(+), 564 deletions(-) delete mode 100644 src/Stryker.Core/Stryker.Core.UnitTest/Mutators/RegexStringSyntaxAttributeMutatorTests.cs rename src/Stryker.Core/Stryker.Core.UnitTest/Mutators/{ => Strings}/RegexMutatorTest.cs (60%) create mode 100644 src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/RegexMutatorWithSemanticModelTests.cs rename src/Stryker.Core/Stryker.Core.UnitTest/Mutators/{ => Strings}/StringEmptyMutatorTests.cs (99%) rename src/Stryker.Core/Stryker.Core.UnitTest/Mutators/{ => Strings}/StringMethodMutatorTests.cs (99%) rename src/Stryker.Core/Stryker.Core.UnitTest/Mutators/{ => Strings}/StringMutatorTests.cs (98%) diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/RegexStringSyntaxAttributeMutatorTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/RegexStringSyntaxAttributeMutatorTests.cs deleted file mode 100644 index 1ccc1fef4..000000000 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/RegexStringSyntaxAttributeMutatorTests.cs +++ /dev/null @@ -1,502 +0,0 @@ -using System.IO; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Serilog.Events; -using Stryker.Abstractions; -using Stryker.Abstractions.Mutators; -using Stryker.Abstractions.Options; -using Stryker.Core.InjectedHelpers; -using Stryker.Core.Mutants; - -namespace Stryker.Core.UnitTest.Mutators; - -[TestClass] -public class RegexStringSyntaxAttributeMutatorTests : TestBase -{ - private readonly CodeInjection _injector = new(); - private readonly CsharpMutantOrchestrator _target; - - public RegexStringSyntaxAttributeMutatorTests() => - _target = new CsharpMutantOrchestrator(new MutantPlacer(_injector), - options: new StrykerOptions - { - MutationLevel = MutationLevel.Complete, - OptimizationMode = OptimizationModes.CoverageBasedTest, - ExcludedMutations = [Mutator.Block, Mutator.Statement], - LogOptions = new LogOptions { LogLevel = LogEventLevel.Verbose } - }); - - private async Task ShouldMutateCompiledSourceToExpectedAsync(string actual, string expected) - { - var cSharpParseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview); - var syntaxTree = CSharpSyntaxTree.ParseText(actual, cSharpParseOptions); - - var basePath = Path.GetDirectoryName(typeof(object).Assembly.Location)!; - var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); - var regex = MetadataReference.CreateFromFile(typeof(Regex).Assembly.Location); - var attribute = MetadataReference.CreateFromFile(Path.Combine(basePath, "System.Runtime.dll")); - - Compilation compilation = CSharpCompilation.Create("MyCompilation", - [syntaxTree], [mscorlib, regex, attribute], - new CSharpCompilationOptions(OutputKind - .DynamicallyLinkedLibrary)); - - var actualNode = _target.Mutate(syntaxTree, compilation.GetSemanticModel(syntaxTree)); - actual = (await actualNode.GetRootAsync()).ToFullString(); - actual = actual.Replace(_injector.HelperNamespace, "StrykerNamespace"); - actualNode = CSharpSyntaxTree.ParseText(actual, cSharpParseOptions); - var expectedNode = CSharpSyntaxTree.ParseText(expected); - actualNode.ShouldBeSemantically(expectedNode); - actualNode.ShouldNotContainErrors(); - } - - - [TestMethod] - public Task ShouldMutateRegexStaticMethods() - { - var source = """ - // Stryker disable block,statement - using System.Text.RegularExpressions; - namespace StrykerNet.UnitTest.Mutants.TestResources; - class RegexClass { - bool A(string input) { - return Regex.IsMatch(input, @"^abc"); - } - } - """; - - var expected = """ - // Stryker disable block,statement - using System.Text.RegularExpressions; - namespace StrykerNet.UnitTest.Mutants.TestResources; - class RegexClass { - bool A(string input) { - return Regex.IsMatch(input, (StrykerNamespace.MutantControl.IsActive(1)?"abc":@"^abc")); - } - } - """; - return ShouldMutateCompiledSourceToExpectedAsync(source, expected); - } - - [TestMethod] - public Task ShouldMutateCustomRegexMethods() - { - var source = """ - // Stryker disable block,statement - using System.Diagnostics.CodeAnalysis; - public class C { - public void M() { - Call("^abc"); - } - - public static void Call([StringSyntax(StringSyntaxAttribute.Regex)]string s) { - - } - } - """; - - var expected = - """ - // Stryker disable block,statement - using System.Diagnostics.CodeAnalysis; - public class C { - public void M() { - Call((StrykerNamespace.MutantControl.IsActive(2)?"abc":"^abc")); - } - public static void Call([StringSyntax(StringSyntaxAttribute.Regex)]string s) { - - } - } - """; - - return ShouldMutateCompiledSourceToExpectedAsync(source, expected); - } - - [TestMethod] - public Task ShouldNotMutateCustomNonRegexMethods() - { - var source = """ - // Stryker disable block,statement - using System.Diagnostics.CodeAnalysis; - public class C { - public void M() { - Call("^abc"); - } - - public static void Call(string s) { - - } - } - """; - - var expected = - """ - // Stryker disable block,statement - using System.Diagnostics.CodeAnalysis; - public class C { - public void M() { - Call((StrykerNamespace.MutantControl.IsActive(2)?"":"^abc")); - } - public static void Call(string s) {} - } - """; - - return ShouldMutateCompiledSourceToExpectedAsync(source, expected); - } - - [TestMethod] - public Task ShouldNotMutateUnrelatedMethods() - { - var source = """ - // Stryker disable block,statement - using System.Diagnostics.CodeAnalysis; - public class C { - public void M() { - Call(false); - } - - public static void Call(bool s) { - - } - } - """; - - var expected = - """ - // Stryker disable block,statement - using System.Diagnostics.CodeAnalysis; - public class C { - public void M() { - Call((StrykerNamespace.MutantControl.IsActive(2)?true:false)); - } - public static void Call(bool s) {} - } - """; - - return ShouldMutateCompiledSourceToExpectedAsync(source, expected); - } - - [TestMethod] - public Task ShouldMutateRegexFields() - { - var source = """ - using System.Diagnostics.CodeAnalysis; - public class C { - [StringSyntax(StringSyntaxAttribute.Regex)] - public string RegexPattern = "^abc"; - } - """; - - var expected = - """ - using System.Diagnostics.CodeAnalysis; - public class C { - [StringSyntax(StringSyntaxAttribute.Regex)] - public string RegexPattern = (StrykerNamespace.MutantControl.IsActive(0)?"abc":"^abc"); - } - """; - - return ShouldMutateCompiledSourceToExpectedAsync(source, expected); - } - - [TestMethod] - public Task ShouldMutateRegexProperties() - { - var source = """ - using System.Diagnostics.CodeAnalysis; - public class C { - [StringSyntax(StringSyntaxAttribute.Regex)] - public string RegexPattern => "^abc"; - } - """; - - var expected = - """ - using System.Diagnostics.CodeAnalysis; - public class C { - [StringSyntax(StringSyntaxAttribute.Regex)] - public string RegexPattern => (StrykerNamespace.MutantControl.IsActive(0)?"abc":"^abc"); - } - """; - - return ShouldMutateCompiledSourceToExpectedAsync(source, expected); - } - - [TestMethod] - public Task ShouldNotApplyRegexMutationToNormalFields() - { - var source = """ - using System.Diagnostics.CodeAnalysis; - public class C { - public string RegexPattern = "^abc"; - } - """; - - var expected = - """ - using System.Diagnostics.CodeAnalysis; - public class C { - public string RegexPattern = (StrykerNamespace.MutantControl.IsActive(0)?"":"^abc"); - } - """; - - return ShouldMutateCompiledSourceToExpectedAsync(source, expected); - } - - [TestMethod] - public Task ShouldNotMutateOtherFields() - { - var source = """ - using System.Diagnostics.CodeAnalysis; - public class C { - public bool RegexPattern = true; - } - """; - - var expected = - """ - using System.Diagnostics.CodeAnalysis; - public class C { - public bool RegexPattern = (StrykerNamespace.MutantControl.IsActive(0)?false:true); - } - """; - - return ShouldMutateCompiledSourceToExpectedAsync(source, expected); - } - - [TestMethod] - public Task ShouldMutateImplicitRegexConstructor() - { - var source = """ - using System.Diagnostics.CodeAnalysis; - public class C { - public static Regex RegexPattern = new("^abc"); - } - """; - - var expected = - """ - using System.Diagnostics.CodeAnalysis; - public class C { - public static Regex RegexPattern = StrykerNamespace.MutantContext.TrackValue(()=>new((StrykerNamespace.MutantControl.IsActive(0)?"abc":"^abc"))); - } - """; - - return ShouldMutateCompiledSourceToExpectedAsync(source, expected); - } - - [TestMethod] - public Task ShouldMutateRegexFieldAssignment() - { - var source = """ - // Stryker disable block,statement - using System.Diagnostics.CodeAnalysis; - public class C { - [StringSyntax(StringSyntaxAttribute.Regex)] - public string RegexPattern; - - public void M() { - RegexPattern = "^abc"; - } - } - """; - - var expected = - """ - // Stryker disable block,statement - using System.Diagnostics.CodeAnalysis; - public class C { - [StringSyntax(StringSyntaxAttribute.Regex)] - public string RegexPattern; - - public void M() { - RegexPattern = (StrykerNamespace.MutantControl.IsActive(1)?"abc":"^abc"); - } - } - """; - - return ShouldMutateCompiledSourceToExpectedAsync(source, expected); - } - - [TestMethod] - public Task ShouldMutateRegexPropertyAssignment() - { - var source = """ - // Stryker disable block,statement - using System.Diagnostics.CodeAnalysis; - public class C { - [StringSyntax(StringSyntaxAttribute.Regex)] - public string RegexPattern { get; set; } - - public void M() { - RegexPattern = "^abc"; - } - } - """; - - var expected = - """ - // Stryker disable block,statement - using System.Diagnostics.CodeAnalysis; - public class C { - [StringSyntax(StringSyntaxAttribute.Regex)] - public string RegexPattern { get; set; } - - public void M() { - RegexPattern = (StrykerNamespace.MutantControl.IsActive(1)?"abc":"^abc"); - } - } - """; - - return ShouldMutateCompiledSourceToExpectedAsync(source, expected); - } - - [TestMethod] - public Task ShouldNotMutateNonRegexFieldAssignment() - { - var source = """ - // Stryker disable block,statement - using System.Diagnostics.CodeAnalysis; - public class C { - public string RegexPattern; - - public void M() { - RegexPattern = "^abc"; - } - } - """; - - var expected = - """ - // Stryker disable block,statement - using System.Diagnostics.CodeAnalysis; - public class C { - public string RegexPattern; - - public void M() { - RegexPattern = (StrykerNamespace.MutantControl.IsActive(1)?"":"^abc"); - } - } - """; - - return ShouldMutateCompiledSourceToExpectedAsync(source, expected); - } - - [TestMethod] - public Task ShouldNotMutateNonRegexPropertyAssignment() - { - var source = """ - // Stryker disable block,statement - using System.Diagnostics.CodeAnalysis; - public class C { - public string RegexPattern { get; set; } - - public void M() { - RegexPattern = "^abc"; - } - } - """; - - var expected = - """ - // Stryker disable block,statement - using System.Diagnostics.CodeAnalysis; - public class C { - public string RegexPattern { get; set; } - - public void M() { - RegexPattern = (StrykerNamespace.MutantControl.IsActive(1)?"":"^abc"); - } - } - """; - - return ShouldMutateCompiledSourceToExpectedAsync(source, expected); - } - - [TestMethod] - public Task ShouldMutateFullyQualifiedAttributeCustomRegexMethod() - { - var source = """ - // Stryker disable block,statement - public class C { - public void M() { - Call("^abc"); - } - - public static void Call([System.Diagnostics.CodeAnalysis.StringSyntax(System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.Regex)]string s) { - - } - } - """; - - var expected = - """ - // Stryker disable block,statement - public class C { - public void M() { - Call((StrykerNamespace.MutantControl.IsActive(2)?"abc":"^abc")); - } - public static void Call([System.Diagnostics.CodeAnalysis.StringSyntax(System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.Regex)]string s) { - - } - } - """; - - return ShouldMutateCompiledSourceToExpectedAsync(source, expected); - } - - [TestMethod] - public Task ShouldMutateFullyQualifiedAttributeOnFieldAssignment() - { - var source = """ - // Stryker disable block,statement - public class C { - [System.Diagnostics.CodeAnalysis.StringSyntax(System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.Regex)] - public string RegexPattern; - - public void M() { - RegexPattern = "^abc"; - } - } - """; - - var expected = - """ - // Stryker disable block,statement - public class C { - [System.Diagnostics.CodeAnalysis.StringSyntax(System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.Regex)] - public string RegexPattern; - - public void M() { - RegexPattern = (StrykerNamespace.MutantControl.IsActive(1)?"abc":"^abc"); - } - } - """; - - return ShouldMutateCompiledSourceToExpectedAsync(source, expected); - } - - [TestMethod] - public Task ShouldMutateFullyQualifiedAttributeOnFieldInitialisation() - { - var source = """ - public class C { - [System.Diagnostics.CodeAnalysis.StringSyntax(System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.Regex)] - public string RegexPattern = "^abc"; - } - """; - - var expected = - """ - public class C { - [System.Diagnostics.CodeAnalysis.StringSyntax(System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.Regex)] - public string RegexPattern = (StrykerNamespace.MutantControl.IsActive(0)?"abc":"^abc"); - } - """; - - return ShouldMutateCompiledSourceToExpectedAsync(source, expected); - } -} diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/RegexMutatorTest.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/RegexMutatorTest.cs similarity index 60% rename from src/Stryker.Core/Stryker.Core.UnitTest/Mutators/RegexMutatorTest.cs rename to src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/RegexMutatorTest.cs index a9357c8b8..45d32fd91 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/RegexMutatorTest.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/RegexMutatorTest.cs @@ -6,11 +6,18 @@ using Stryker.Abstractions.Mutators; using Stryker.Core.Mutators; -namespace Stryker.Core.UnitTest.Mutators; +namespace Stryker.Core.UnitTest.Mutators.Strings; [TestClass] public class RegexMutatorTest : TestBase { + + private static LiteralExpressionSyntax ParseExpression(string text) + { + var objectCreationExpression = SyntaxFactory.ParseExpression(text) as ObjectCreationExpressionSyntax; + return objectCreationExpression?.DescendantNodesAndSelf().OfType().FirstOrDefault(); + } + [TestMethod] public void ShouldBeMutationLevelAdvanced() { @@ -21,10 +28,10 @@ public void ShouldBeMutationLevelAdvanced() [TestMethod] public void ShouldMutateStringLiteralInRegexConstructor() { - var objectCreationExpression = SyntaxFactory.ParseExpression("new Regex(@\"^abc\")") as ObjectCreationExpressionSyntax; + var literalExpression = ParseExpression("new Regex(@\"^abc\")"); var target = new StringMutator(); - var result = target.ApplyMutations(objectCreationExpression.DescendantNodesAndSelf().OfType().First(), null, MutationLevel.Advanced); + var result = target.ApplyMutations(literalExpression, null, MutationLevel.Advanced); var mutation = result.ShouldHaveSingleItem(); @@ -36,10 +43,10 @@ public void ShouldMutateStringLiteralInRegexConstructor() [TestMethod] public void ShouldMutateStringLiteralInRegexConstructorWithFullName() { - var objectCreationExpression = SyntaxFactory.ParseExpression("new System.Text.RegularExpressions.Regex(@\"^abc\")") as ObjectCreationExpressionSyntax; + var literalExpression = ParseExpression("new System.Text.RegularExpressions.Regex(@\"^abc\")"); var target = new StringMutator(); - var result = target.ApplyMutations(objectCreationExpression.DescendantNodesAndSelf().OfType().First(), null, MutationLevel.Advanced); + var result = target.ApplyMutations(literalExpression, null, MutationLevel.Advanced); var mutation = result.ShouldHaveSingleItem(); @@ -51,20 +58,30 @@ public void ShouldMutateStringLiteralInRegexConstructorWithFullName() [TestMethod] public void ShouldNotMutateStringLiteralInOtherConstructor() { - var objectCreationExpression = SyntaxFactory.ParseExpression("new Other(@\"^abc\")") as ObjectCreationExpressionSyntax; + var literalExpression = ParseExpression("new Other(@\"^abc\")"); + var target = new StringMutator(); + var result = target.ApplyMutations(literalExpression, null, MutationLevel.Advanced); + + result.Where(a => a.Type == Mutator.Regex).ShouldBeEmpty(); + } + + [TestMethod] + public void ShouldNotMutateAtLowerMutationLevel() + { + var literalExpression = ParseExpression("new Other(@\"^abc\")"); var target = new StringMutator(); - var result = target.ApplyMutations(objectCreationExpression.DescendantNodesAndSelf().OfType().First(), null, MutationLevel.Advanced); + var result = target.ApplyMutations(literalExpression, null, MutationLevel.Standard); - result.Where(a=>a.Type == Mutator.Regex).ShouldBeEmpty(); + result.Where(a => a.Type == Mutator.Regex).ShouldBeEmpty(); } [TestMethod] public void ShouldMutateStringLiteralMultipleTimes() { - var objectCreationExpression = SyntaxFactory.ParseExpression("new Regex(@\"^abc$\")") as ObjectCreationExpressionSyntax; + var literalExpression = ParseExpression("new Regex(@\"^abc$\")"); var target = new StringMutator(); - var result = target.ApplyMutations(objectCreationExpression.DescendantNodesAndSelf().OfType().First(), null, MutationLevel.Advanced); + var result = target.ApplyMutations(literalExpression, null, MutationLevel.Advanced); result.Count().ShouldBe(2); result.ShouldAllBe(mutant => mutant.DisplayName == "Regex anchor removal mutation"); @@ -77,10 +94,10 @@ public void ShouldMutateStringLiteralMultipleTimes() [TestMethod] public void ShouldMutateStringLiteralAsNamedArgumentPatternInRegexConstructor() { - var objectCreationExpression = SyntaxFactory.ParseExpression("new Regex(options: RegexOptions.None, pattern: @\"^abc\")") as ObjectCreationExpressionSyntax; + var literalExpression = ParseExpression("new Regex(options: RegexOptions.None, pattern: @\"^abc\")"); var target = new StringMutator(); - var result = target.ApplyMutations(objectCreationExpression.DescendantNodesAndSelf().OfType().First(), null, MutationLevel.Advanced); + var result = target.ApplyMutations(literalExpression, null, MutationLevel.Advanced); var mutation = result.ShouldHaveSingleItem(); diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/RegexMutatorWithSemanticModelTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/RegexMutatorWithSemanticModelTests.cs new file mode 100644 index 000000000..08031f4e5 --- /dev/null +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/RegexMutatorWithSemanticModelTests.cs @@ -0,0 +1,382 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using Stryker.Abstractions; +using Stryker.Abstractions.Mutants; +using Stryker.Abstractions.Mutators; +using Stryker.Abstractions.Options; +using Stryker.Core.Mutators; + +namespace Stryker.Core.UnitTest.Mutators.Strings; + +[TestClass] +public class RegexMutatorWithSemanticModelTests : TestBase +{ + private static readonly IStrykerOptions _options = new StrykerOptions { MutationLevel = MutationLevel.Advanced }; + + private static (SemanticModel semanticModel, LiteralExpressionSyntax expression) + CreateSemanticModelFromExpression(string input) + { + // Parse the code into a syntax tree + var syntaxTree = CSharpSyntaxTree.ParseText(input); + + // Create a compilation that contains the syntax tree + var basePath = Path.GetDirectoryName(typeof(object).Assembly.Location)!; + var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); + var regex = MetadataReference.CreateFromFile(typeof(Regex).Assembly.Location); + var attribute = MetadataReference.CreateFromFile(Path.Combine(basePath, "System.Runtime.dll")); + var compilation = CSharpCompilation.Create("TestAssembly") + .WithOptions(new CSharpCompilationOptions(OutputKind + .DynamicallyLinkedLibrary)) + .AddReferences(mscorlib) + .AddReferences(regex) + .AddReferences(attribute) + .AddSyntaxTrees(syntaxTree); + + // Get the semantic model from the compilation + var semanticModel = compilation.GetSemanticModel(syntaxTree); + + var expression = syntaxTree.GetRoot().DescendantNodes().OfType().First(); + + return (semanticModel, expression); + } + + private static void ValidateMutation(IEnumerable result) + { + var mutation = result.ShouldHaveSingleItem(); + + mutation.Type.ShouldBe(Mutator.Regex); + mutation.DisplayName.ShouldBe("Regex anchor removal mutation"); + + var replacement = mutation.ReplacementNode.ShouldBeOfType(); + replacement.Token.ValueText.ShouldBe("abc"); + } + + [TestMethod] + public void ShouldMutateRegexStaticMethods() + { + var source = """ + using System.Text.RegularExpressions; + namespace StrykerNet.UnitTest.Mutants.TestResources; + class RegexClass { + bool A(string input) { + return Regex.IsMatch(input, @"^abc"); + } + } + """; + + var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(source); + var target = new StringMutator(); + var result = target.Mutate(expressionSyntax, semanticModel, _options); + + ValidateMutation(result); + } + + [TestMethod] + public void ShouldMutateCustomRegexMethods() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + public class C { + public void M() { + Call("^abc"); + } + + public static void Call([StringSyntax(StringSyntaxAttribute.Regex)]string s) { + + } + } + """; + + var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(source); + var target = new StringMutator(); + var result = target.Mutate(expressionSyntax, semanticModel, _options); + + ValidateMutation(result); + } + + [TestMethod] + public void ShouldNotMutateCustomNonRegexMethods() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + public class C { + public void M() { + Call("^abc"); + } + + public static void Call(string s) { + + } + } + """; + + var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(source); + var target = new StringMutator(); + var result = target.Mutate(expressionSyntax, semanticModel, _options); + + result.Where(a => a.Type == Mutator.Regex).ShouldBeEmpty(); + } + + [TestMethod] + public void ShouldNotMutateUnrelatedMethods() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + public class C { + public void M() { + Call(false); + } + + public static void Call(bool s) { + + } + } + """; + + var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(source); + var target = new StringMutator(); + var result = target.Mutate(expressionSyntax, semanticModel, _options); + + result.Where(a => a.Type == Mutator.Regex).ShouldBeEmpty(); + } + + [TestMethod] + public void ShouldMutateRegexFields() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + public class C { + [StringSyntax(StringSyntaxAttribute.Regex)] + public string RegexPattern = "^abc"; + } + """; + + var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(source); + var target = new StringMutator(); + var result = target.Mutate(expressionSyntax, semanticModel, _options); + + ValidateMutation(result); + } + + [TestMethod] + public void ShouldMutateRegexProperties() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + public class C { + [StringSyntax(StringSyntaxAttribute.Regex)] + public string RegexPattern => "^abc"; + } + """; + + var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(source); + var target = new StringMutator(); + var result = target.Mutate(expressionSyntax, semanticModel, _options); + + ValidateMutation(result); + } + + [TestMethod] + public void ShouldNotApplyRegexMutationToNormalFields() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + public class C { + public string RegexPattern = "^abc"; + } + """; + + var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(source); + var target = new StringMutator(); + var result = target.Mutate(expressionSyntax, semanticModel, _options); + + result.Where(a => a.Type == Mutator.Regex).ShouldBeEmpty(); + } + + [TestMethod] + public void ShouldNotMutateOtherFields() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + public class C { + public bool RegexPattern = true; + } + """; + + var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(source); + var target = new StringMutator(); + var result = target.Mutate(expressionSyntax, semanticModel, _options); + + result.Where(a => a.Type == Mutator.Regex).ShouldBeEmpty(); + } + + [TestMethod] + public void ShouldMutateImplicitRegexConstructor() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + public class C { + public static Regex RegexPattern = new("^abc"); + } + """; + + var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(source); + var target = new StringMutator(); + var result = target.Mutate(expressionSyntax, semanticModel, _options); + + ValidateMutation(result); + } + + [TestMethod] + public void ShouldMutateRegexFieldAssignment() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + public class C { + [StringSyntax(StringSyntaxAttribute.Regex)] + public string RegexPattern; + + public void M() { + RegexPattern = "^abc"; + } + } + """; + + var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(source); + var target = new StringMutator(); + var result = target.Mutate(expressionSyntax, semanticModel, _options); + + ValidateMutation(result); + } + + [TestMethod] + public void ShouldMutateRegexPropertyAssignment() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + public class C { + [StringSyntax(StringSyntaxAttribute.Regex)] + public string RegexPattern { get; set; } + + public void M() { + RegexPattern = "^abc"; + } + } + """; + + var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(source); + var target = new StringMutator(); + var result = target.Mutate(expressionSyntax, semanticModel, _options); + + ValidateMutation(result); + } + + [TestMethod] + public void ShouldNotMutateNonRegexFieldAssignment() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + public class C { + public string RegexPattern; + + public void M() { + RegexPattern = "^abc"; + } + } + """; + + var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(source); + var target = new StringMutator(); + var result = target.Mutate(expressionSyntax, semanticModel, _options); + + result.Where(a => a.Type == Mutator.Regex).ShouldBeEmpty(); + } + + [TestMethod] + public void ShouldNotMutateNonRegexPropertyAssignment() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + public class C { + public string RegexPattern { get; set; } + + public void M() { + RegexPattern = "^abc"; + } + } + """; + + var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(source); + var target = new StringMutator(); + var result = target.Mutate(expressionSyntax, semanticModel, _options); + + result.Where(a => a.Type == Mutator.Regex).ShouldBeEmpty(); + } + + [TestMethod] + public void ShouldMutateFullyQualifiedAttributeCustomRegexMethod() + { + var source = """ + public class C { + public void M() { + Call("^abc"); + } + + public static void Call([System.Diagnostics.CodeAnalysis.StringSyntax(System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.Regex)]string s) { + + } + } + """; + + var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(source); + var target = new StringMutator(); + var result = target.Mutate(expressionSyntax, semanticModel, _options); + + ValidateMutation(result); + } + + [TestMethod] + public void ShouldMutateFullyQualifiedAttributeOnFieldAssignment() + { + var source = """ + public class C { + [System.Diagnostics.CodeAnalysis.StringSyntax(System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.Regex)] + public string RegexPattern; + + public void M() { + RegexPattern = "^abc"; + } + } + """; + + var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(source); + var target = new StringMutator(); + var result = target.Mutate(expressionSyntax, semanticModel, _options); + + ValidateMutation(result); + } + + [TestMethod] + public void ShouldMutateFullyQualifiedAttributeOnFieldInitialisation() + { + var source = """ + public class C { + [System.Diagnostics.CodeAnalysis.StringSyntax(System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.Regex)] + public string RegexPattern = "^abc"; + } + """; + + var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(source); + var target = new StringMutator(); + var result = target.Mutate(expressionSyntax, semanticModel, _options); + + ValidateMutation(result); + } +} diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/StringEmptyMutatorTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/StringEmptyMutatorTests.cs similarity index 99% rename from src/Stryker.Core/Stryker.Core.UnitTest/Mutators/StringEmptyMutatorTests.cs rename to src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/StringEmptyMutatorTests.cs index 7c3868852..907d41aa6 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/StringEmptyMutatorTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/StringEmptyMutatorTests.cs @@ -7,7 +7,7 @@ using Stryker.Abstractions.Mutators; using Stryker.Core.Mutators; -namespace Stryker.Core.UnitTest.Mutators; +namespace Stryker.Core.UnitTest.Mutators.Strings; [TestClass] public class StringEmptyMutatorTests : TestBase diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/StringMethodMutatorTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/StringMethodMutatorTests.cs similarity index 99% rename from src/Stryker.Core/Stryker.Core.UnitTest/Mutators/StringMethodMutatorTests.cs rename to src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/StringMethodMutatorTests.cs index 9739cc82a..e97b3b775 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/StringMethodMutatorTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/StringMethodMutatorTests.cs @@ -7,7 +7,7 @@ using Stryker.Abstractions.Mutators; using Stryker.Core.Mutators; -namespace Stryker.Core.UnitTest.Mutators; +namespace Stryker.Core.UnitTest.Mutators.Strings; [TestClass] public class StringMethodMutatorTests : TestBase diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/StringMutatorTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/StringMutatorTests.cs similarity index 98% rename from src/Stryker.Core/Stryker.Core.UnitTest/Mutators/StringMutatorTests.cs rename to src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/StringMutatorTests.cs index 31bcda68d..059624235 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/StringMutatorTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/StringMutatorTests.cs @@ -6,7 +6,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Stryker.Core.Mutators; -namespace Stryker.Core.UnitTest.Mutators; +namespace Stryker.Core.UnitTest.Mutators.Strings; [TestClass] public class StringMutatorTests : TestBase diff --git a/src/Stryker.Core/Stryker.Core/Mutators/StringMutator.cs b/src/Stryker.Core/Stryker.Core/Mutators/StringMutator.cs index 0694c61f3..b4b874af9 100644 --- a/src/Stryker.Core/Stryker.Core/Mutators/StringMutator.cs +++ b/src/Stryker.Core/Stryker.Core/Mutators/StringMutator.cs @@ -13,6 +13,7 @@ using Stryker.Abstractions.Mutators; using Stryker.Abstractions.Options; using Stryker.RegexMutators; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace Stryker.Core.Mutators; @@ -40,54 +41,65 @@ public IEnumerable ApplyMutations(LiteralExpressionSyntax node, Semant { if (IsRegexString(node, semanticModel)) { - if (RegexMutationLevel > mutationLevel) - { - yield break; - } + return ApplyRegexMutations(node, mutationLevel); + } - var currentValue = node.Token.ValueText; - var regexMutantOrchestrator = new RegexMutantOrchestrator(currentValue); - var replacementValues = regexMutantOrchestrator.Mutate(); + if (OtherMutationLevel <= mutationLevel && !IsGuidType(node.Parent?.Parent?.Parent, semanticModel)) + { + return [ApplyStringMutations(node)]; + } - foreach (var regexMutation in replacementValues) - { - try - { - _ = new Regex(regexMutation.ReplacementPattern); - } - catch (ArgumentException exception) - { - Logger?.LogDebug( - "RegexMutator created mutation {CurrentValue} -> {ReplacementPattern} which is an invalid regular expression:\n{Message}", - currentValue, regexMutation.ReplacementPattern, exception.Message); - continue; - } + return []; + } - yield return new Mutation - { - OriginalNode = node, - ReplacementNode = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, - SyntaxFactory.Literal(regexMutation - .ReplacementPattern)), - DisplayName = regexMutation.DisplayName, - Type = Mutator.Regex, - Description = regexMutation.Description - }; - } + private static Mutation ApplyStringMutations(LiteralExpressionSyntax node) + { + var currentValue = (string)node.Token.Value; + var replacementValue = currentValue == "" ? "Stryker was here!" : ""; + + return new Mutation + { + OriginalNode = node, + ReplacementNode = LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(replacementValue)), + DisplayName = "String mutation", + Type = Mutator.String + }; + } + + private IEnumerable ApplyRegexMutations(LiteralExpressionSyntax node, MutationLevel mutationLevel) + { + if (RegexMutationLevel > mutationLevel) + { + yield break; } - else if (OtherMutationLevel <= mutationLevel && !IsSpecialType(node.Parent?.Parent?.Parent)) + + var currentValue = node.Token.ValueText; + var regexMutantOrchestrator = new RegexMutantOrchestrator(currentValue); + var replacementValues = regexMutantOrchestrator.Mutate(); + + foreach (var regexMutation in replacementValues) { - var currentValue = (string)node.Token.Value; - var replacementValue = currentValue == "" ? "Stryker was here!" : ""; + try + { + _ = new Regex(regexMutation.ReplacementPattern); + } + catch (ArgumentException exception) + { + Logger?.LogDebug( + "RegexMutator created mutation {CurrentValue} -> {ReplacementPattern} which is an invalid regular expression:\n{Message}", + currentValue, regexMutation.ReplacementPattern, exception.Message); + continue; + } yield return new Mutation { OriginalNode = node, ReplacementNode = - SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, - SyntaxFactory.Literal(replacementValue)), - DisplayName = "String mutation", - Type = Mutator.String + LiteralExpression(SyntaxKind.StringLiteralExpression, + Literal(regexMutation.ReplacementPattern)), + DisplayName = regexMutation.DisplayName, + Type = Mutator.Regex, + Description = regexMutation.Description }; } } @@ -102,11 +114,9 @@ private static bool IsRegexString(SyntaxNode node, SemanticModel semanticModel) return IsCtorOfType(ctor, typeof(Regex)); case ImplicitObjectCreationExpressionSyntax ctor: return IsCtorOfType(ctor, typeof(Regex), semanticModel); - case ArgumentSyntax argument when semanticModel is not null && - argument.Parent?.Parent is InvocationExpressionSyntax parentInvocation - : - var invocationOp = semanticModel?.GetOperation(parentInvocation) as IInvocationOperation; - var argumentOp = invocationOp?.Arguments.SingleOrDefault(a => a.Syntax == argument); + case ArgumentSyntax { Parent.Parent: InvocationExpressionSyntax parentInvocation } argument + when semanticModel?.GetOperation(parentInvocation) is IInvocationOperation invocationOp: + var argumentOp = invocationOp.Arguments.SingleOrDefault(a => a.Syntax == argument); return argumentOp?.Parameter?.Type.Name is "String" && argumentOp.Parameter.GetAttributes().Any(IsRegexSyntaxAttribute); @@ -119,11 +129,11 @@ private static bool IsRegexString(SyntaxNode node, SemanticModel semanticModel) { Type.SpecialType: SpecialType.System_String } expressionOp: - return expressionOp?.Target switch + return expressionOp.Target switch { IFieldReferenceOperation field => field.Field.GetAttributes().Any(IsRegexSyntaxAttribute), IPropertyReferenceOperation property => property.Property.GetAttributes() - .Any(IsRegexSyntaxAttribute), + .Any(IsRegexSyntaxAttribute), _ => false }; } @@ -136,7 +146,7 @@ private static bool IsRegexSyntaxAttribute(AttributeSyntax attributeSyntax) => IsStringSyntaxAttribute(attributeSyntax.Name) && attributeSyntax.ArgumentList?.Arguments is [var arg] && (arg.Expression is - LiteralExpressionSyntax { Token.Text: "Regex" } or + LiteralExpressionSyntax { Token.ValueText: "Regex" } or IdentifierNameSyntax { Identifier.Text: "Regex" } || (arg.Expression is MemberAccessExpressionSyntax { @@ -208,9 +218,10 @@ private static bool IsRegexSyntaxAttribute(AttributeData attributeData) => (attributeData.AttributeClass?.Name.Equals("StringSyntaxAttribute") ?? false) && attributeData.ConstructorArguments.FirstOrDefault().Value is StringSyntaxAttribute.Regex; - private static bool IsSpecialType(SyntaxNode root) => root switch + private static bool IsGuidType(SyntaxNode root, SemanticModel semanticModel) => root switch { ObjectCreationExpressionSyntax ctor => IsCtorOfType(ctor, typeof(Guid)), + ImplicitObjectCreationExpressionSyntax ctor => IsCtorOfType(ctor, typeof(Guid), semanticModel), _ => false }; From 15592ecca44c2a3f4dae24d227490ab201f182e5 Mon Sep 17 00:00:00 2001 From: Pentiva Date: Wed, 6 Nov 2024 10:01:58 +0000 Subject: [PATCH 6/9] Improve mutation score --- .../Mutators/Strings/RegexMutatorTest.cs | 13 ++ .../RegexMutatorWithSemanticModelTests.cs | 112 ++++++++++++++++-- .../Stryker.Core/Mutators/StringMutator.cs | 22 ++-- 3 files changed, 126 insertions(+), 21 deletions(-) diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/RegexMutatorTest.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/RegexMutatorTest.cs index 45d32fd91..2453e5718 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/RegexMutatorTest.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/RegexMutatorTest.cs @@ -25,6 +25,19 @@ public void ShouldBeMutationLevelAdvanced() target.RegexMutationLevel.ShouldBe(MutationLevel.Advanced); } + [TestMethod] + [DataRow(MutationLevel.Basic)] + [DataRow(MutationLevel.Standard)] + public void ShouldNotMutateWithLowerMutationLevel(MutationLevel level) + { + var literalExpression = ParseExpression("new Regex(@\"^abc\")"); + var target = new StringMutator(); + + var result = target.ApplyMutations(literalExpression, null, level); + + result.ShouldBeEmpty(); + } + [TestMethod] public void ShouldMutateStringLiteralInRegexConstructor() { diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/RegexMutatorWithSemanticModelTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/RegexMutatorWithSemanticModelTests.cs index 08031f4e5..ac08fdbe8 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/RegexMutatorWithSemanticModelTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/RegexMutatorWithSemanticModelTests.cs @@ -33,7 +33,7 @@ private static (SemanticModel semanticModel, LiteralExpressionSyntax expression) var attribute = MetadataReference.CreateFromFile(Path.Combine(basePath, "System.Runtime.dll")); var compilation = CSharpCompilation.Create("TestAssembly") .WithOptions(new CSharpCompilationOptions(OutputKind - .DynamicallyLinkedLibrary)) + .DynamicallyLinkedLibrary)) .AddReferences(mscorlib) .AddReferences(regex) .AddReferences(attribute) @@ -42,7 +42,8 @@ private static (SemanticModel semanticModel, LiteralExpressionSyntax expression) // Get the semantic model from the compilation var semanticModel = compilation.GetSemanticModel(syntaxTree); - var expression = syntaxTree.GetRoot().DescendantNodes().OfType().First(); + var expression = syntaxTree.GetRoot().DescendantNodes().OfType() + .First(a => a.IsKind(SyntaxKind.StringLiteralExpression)); return (semanticModel, expression); } @@ -102,7 +103,7 @@ public static void Call([StringSyntax(StringSyntaxAttribute.Regex)]string s) { } [TestMethod] - public void ShouldNotMutateCustomNonRegexMethods() + public void ShouldMutateCustomRegexMethodsWithManualSyntax() { var source = """ using System.Diagnostics.CodeAnalysis; @@ -111,7 +112,30 @@ public void M() { Call("^abc"); } - public static void Call(string s) { + public static void Call([StringSyntax("Regex")]string s) { + + } + } + """; + + var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(source); + var target = new StringMutator(); + var result = target.Mutate(expressionSyntax, semanticModel, _options); + + ValidateMutation(result); + } + + [TestMethod] + public void ShouldNotMutateNonRegexParametersWithSimilarSyntax() + { + var source = """ + using System.ComponentModel; + public class C { + public void M() { + Call("^abc"); + } + + public static void Call([DefaultValue("Regex")]string s) { } } @@ -125,16 +149,61 @@ public static void Call(string s) { } [TestMethod] - public void ShouldNotMutateUnrelatedMethods() + public void ShouldNotMutateBadNonRegexMethod() { var source = """ using System.Diagnostics.CodeAnalysis; public class C { public void M() { - Call(false); + Call("^abc"); + } + + public static void Call([StringSyntaxAttribute]string s) { + + } + } + """; + + var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(source); + var target = new StringMutator(); + var result = target.Mutate(expressionSyntax, semanticModel, _options); + + result.Where(a => a.Type == Mutator.Regex).ShouldBeEmpty(); + } + + [TestMethod] + public void ShouldNotMutateNonRegexParameters() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + public class C { + public void M() { + Call("^abc"); } - public static void Call(bool s) { + public static void Call([StringSyntaxAttribute(StringSyntaxAttribute.Json)]string s) { + + } + } + """; + + var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(source); + var target = new StringMutator(); + var result = target.Mutate(expressionSyntax, semanticModel, _options); + + result.Where(a => a.Type == Mutator.Regex).ShouldBeEmpty(); + } + + [TestMethod] + public void ShouldNotMutateCustomNonRegexMethods() + { + var source = """ + public class C { + public void M() { + Call("^abc"); + } + + public static void Call(string s) { } } @@ -184,11 +253,13 @@ public class C { } [TestMethod] - public void ShouldNotApplyRegexMutationToNormalFields() + public void ShouldMutateRegexFieldsWithMultipleAttributes() { var source = """ + using System.ComponentModel; using System.Diagnostics.CodeAnalysis; public class C { + [StringSyntax(StringSyntaxAttribute.Regex), DefaultValue(false)] public string RegexPattern = "^abc"; } """; @@ -197,16 +268,35 @@ public class C { var target = new StringMutator(); var result = target.Mutate(expressionSyntax, semanticModel, _options); - result.Where(a => a.Type == Mutator.Regex).ShouldBeEmpty(); + ValidateMutation(result); } [TestMethod] - public void ShouldNotMutateOtherFields() + public void ShouldMutateRegexPropertiesWithMultipleAttributes() { var source = """ + using System.ComponentModel; using System.Diagnostics.CodeAnalysis; public class C { - public bool RegexPattern = true; + [StringSyntax(StringSyntaxAttribute.Regex), DefaultValue(false)] + public string RegexPattern => "^abc"; + } + """; + + var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(source); + var target = new StringMutator(); + var result = target.Mutate(expressionSyntax, semanticModel, _options); + + ValidateMutation(result); + } + + [TestMethod] + public void ShouldNotApplyRegexMutationToNormalFields() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + public class C { + public string RegexPattern = "^abc"; } """; diff --git a/src/Stryker.Core/Stryker.Core/Mutators/StringMutator.cs b/src/Stryker.Core/Stryker.Core/Mutators/StringMutator.cs index b4b874af9..8090f8219 100644 --- a/src/Stryker.Core/Stryker.Core/Mutators/StringMutator.cs +++ b/src/Stryker.Core/Stryker.Core/Mutators/StringMutator.cs @@ -125,17 +125,19 @@ private static bool IsRegexString(SyntaxNode node, SemanticModel semanticModel) case PropertyDeclarationSyntax field: return field.AttributeLists.Any(static a => a.Attributes.Any(IsRegexSyntaxAttribute)); case AssignmentExpressionSyntax assignment - when semanticModel?.GetOperation(assignment) is ISimpleAssignmentOperation + when semanticModel?.GetOperation(assignment) is IAssignmentOperation { - Type.SpecialType: SpecialType.System_String - } expressionOp: - return expressionOp.Target switch - { - IFieldReferenceOperation field => field.Field.GetAttributes().Any(IsRegexSyntaxAttribute), - IPropertyReferenceOperation property => property.Property.GetAttributes() - .Any(IsRegexSyntaxAttribute), - _ => false - }; + Type.SpecialType: SpecialType.System_String, + Target: IMemberReferenceOperation + { + Member: var member + } + }: + return member.GetAttributes().Any(IsRegexSyntaxAttribute); + case BlockSyntax: // Early exits + case MemberDeclarationSyntax: + case UsingDirectiveSyntax: + return false; } } From c67d876cd165a8aaa68f1c8e8f55cd87c0201754 Mon Sep 17 00:00:00 2001 From: Pentiva Date: Wed, 6 Nov 2024 13:14:49 +0000 Subject: [PATCH 7/9] Add support for UTF8 string literals Add more tests. Update docs. --- docs/mutations.md | 2 + docs/regex-mutations.md | 2 +- .../Mutants/CsharpMutantOrchestratorTests.cs | 2 +- .../RegexMutatorWithSemanticModelTests.cs | 151 ++++++++++++++++-- .../Mutators/Strings/StringMutatorTests.cs | 36 +++++ .../Stryker.Core/Mutators/StringMutator.cs | 53 +++++- 6 files changed, 223 insertions(+), 23 deletions(-) diff --git a/docs/mutations.md b/docs/mutations.md index 4fe129a1b..2835b0790 100644 --- a/docs/mutations.md +++ b/docs/mutations.md @@ -158,6 +158,8 @@ Do you have a suggestion for a (new) mutator? Feel free to create an [issue](htt | ------------- | ------------- | | `"foo"` | `""` | | `""` | `"Stryker was here!"` | +| `"foo"u8` | `""u8` | +| `""u8` | `"Stryker was here!"u8` | | `$"foo {bar}"` | `$""` | | `@"foo"` | `@""` | | `string.Empty` | `"Stryker was here!"` | diff --git a/docs/regex-mutations.md b/docs/regex-mutations.md index c8f00d345..70ea88b98 100644 --- a/docs/regex-mutations.md +++ b/docs/regex-mutations.md @@ -5,7 +5,7 @@ custom_edit_url: https://github.com/stryker-mutator/stryker-net/edit/master/docs --- Stryker supports a variety of regular expression mutators, which are listed below. Do you have a suggestion for a (new) mutator? Feel free to create an [issue](https://github.com/stryker-mutator/stryker-net/issues)! -Regex mutations are applied to arguments that are annotated with `[StringSyntax(StringSyntaxAttribute.Regex)]` on NET7+, and just the Regex constructor on other frameworks. +Regex mutations are applied to arguments, fields, and properties that are annotated with `[StringSyntax(StringSyntaxAttribute.Regex)]` on NET7+, and just the Regex constructor on prior targets. ## Common tokens | Original | Mutated | diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutants/CsharpMutantOrchestratorTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutants/CsharpMutantOrchestratorTests.cs index af8c38040..dc93f3418 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Mutants/CsharpMutantOrchestratorTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Mutants/CsharpMutantOrchestratorTests.cs @@ -1577,7 +1577,7 @@ int TestMethod() } [TestMethod] - public void ShouldMutatetringsInSwitchExpression() + public void ShouldMutateStringsInSwitchExpression() { var source = @"string TestMethod() { diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/RegexMutatorWithSemanticModelTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/RegexMutatorWithSemanticModelTests.cs index ac08fdbe8..5269febaa 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/RegexMutatorWithSemanticModelTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/RegexMutatorWithSemanticModelTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -29,12 +30,14 @@ private static (SemanticModel semanticModel, LiteralExpressionSyntax expression) // Create a compilation that contains the syntax tree var basePath = Path.GetDirectoryName(typeof(object).Assembly.Location)!; var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); + var ros = MetadataReference.CreateFromFile(typeof(ReadOnlySpan<>).Assembly.Location); var regex = MetadataReference.CreateFromFile(typeof(Regex).Assembly.Location); var attribute = MetadataReference.CreateFromFile(Path.Combine(basePath, "System.Runtime.dll")); var compilation = CSharpCompilation.Create("TestAssembly") .WithOptions(new CSharpCompilationOptions(OutputKind - .DynamicallyLinkedLibrary)) + .DynamicallyLinkedLibrary)) .AddReferences(mscorlib) + .AddReferences(ros) .AddReferences(regex) .AddReferences(attribute) .AddSyntaxTrees(syntaxTree); @@ -48,7 +51,35 @@ private static (SemanticModel semanticModel, LiteralExpressionSyntax expression) return (semanticModel, expression); } - private static void ValidateMutation(IEnumerable result) + private static (SemanticModel semanticModel, IEnumerable expression) + CreateSemanticModelFromExpressions(string input) + { + // Parse the code into a syntax tree + var syntaxTree = CSharpSyntaxTree.ParseText(input); + + // Create a compilation that contains the syntax tree + var basePath = Path.GetDirectoryName(typeof(object).Assembly.Location)!; + var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); + var regex = MetadataReference.CreateFromFile(typeof(Regex).Assembly.Location); + var attribute = MetadataReference.CreateFromFile(Path.Combine(basePath, "System.Runtime.dll")); + var compilation = CSharpCompilation.Create("TestAssembly") + .WithOptions(new CSharpCompilationOptions(OutputKind + .DynamicallyLinkedLibrary)) + .AddReferences(mscorlib) + .AddReferences(regex) + .AddReferences(attribute) + .AddSyntaxTrees(syntaxTree); + + // Get the semantic model from the compilation + var semanticModel = compilation.GetSemanticModel(syntaxTree); + + var expression = syntaxTree.GetRoot().DescendantNodes().OfType() + .Where(a => a.IsKind(SyntaxKind.StringLiteralExpression)); + + return (semanticModel, expression); + } + + private static void ValidateRegexMutation(IEnumerable result) { var mutation = result.ShouldHaveSingleItem(); @@ -76,7 +107,55 @@ bool A(string input) { var target = new StringMutator(); var result = target.Mutate(expressionSyntax, semanticModel, _options); - ValidateMutation(result); + ValidateRegexMutation(result); + } + + [TestMethod] + public void ShouldMutateRegexStaticMethodsMultipleString() + { + var source = """ + using System.Text.RegularExpressions; + namespace StrykerNet.UnitTest.Mutants.TestResources; + class RegexClass { + bool A() { + return Regex.IsMatch("input", @"^abc"); + } + } + """; + + var (semanticModel, expressionSyntaxes) = CreateSemanticModelFromExpressions(source); + var target = new StringMutator(); + + var syntaxes = expressionSyntaxes.ToList(); + var stringResult = target.Mutate(syntaxes[0], semanticModel, _options); + var regexResult = target.Mutate(syntaxes[1], semanticModel, _options); + + stringResult.Where(a => a.Type == Mutator.Regex).ShouldBeEmpty(); + ValidateRegexMutation(regexResult); + } + + [TestMethod] + public void ShouldMutateRegexStaticMethodsMultipleStringNamedParameters() + { + var source = """ + using System.Text.RegularExpressions; + namespace StrykerNet.UnitTest.Mutants.TestResources; + class RegexClass { + bool A() { + return Regex.IsMatch(pattern: @"^abc", input: "input"); + } + } + """; + + var (semanticModel, expressionSyntaxes) = CreateSemanticModelFromExpressions(source); + var target = new StringMutator(); + + var syntaxes = expressionSyntaxes.ToList(); + var regexResult = target.Mutate(syntaxes[0], semanticModel, _options); + var stringResult = target.Mutate(syntaxes[1], semanticModel, _options); + + stringResult.Where(a => a.Type == Mutator.Regex).ShouldBeEmpty(); + ValidateRegexMutation(regexResult); } [TestMethod] @@ -99,7 +178,31 @@ public static void Call([StringSyntax(StringSyntaxAttribute.Regex)]string s) { var target = new StringMutator(); var result = target.Mutate(expressionSyntax, semanticModel, _options); - ValidateMutation(result); + ValidateRegexMutation(result); + } + + [TestMethod] + public void ShouldMutateCustomRegexMethodReadonlySpan() + { + var source = """ + using System; + using System.Diagnostics.CodeAnalysis; + public class C { + public void M() { + Call("^abc"); + } + + public static void Call([StringSyntax(StringSyntaxAttribute.Regex)]ReadOnlySpan s) { + + } + } + """; + + var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(source); + var target = new StringMutator(); + var result = target.Mutate(expressionSyntax, semanticModel, _options); + + ValidateRegexMutation(result); } [TestMethod] @@ -122,7 +225,7 @@ public static void Call([StringSyntax("Regex")]string s) { var target = new StringMutator(); var result = target.Mutate(expressionSyntax, semanticModel, _options); - ValidateMutation(result); + ValidateRegexMutation(result); } [TestMethod] @@ -231,7 +334,7 @@ public class C { var target = new StringMutator(); var result = target.Mutate(expressionSyntax, semanticModel, _options); - ValidateMutation(result); + ValidateRegexMutation(result); } [TestMethod] @@ -249,7 +352,25 @@ public class C { var target = new StringMutator(); var result = target.Mutate(expressionSyntax, semanticModel, _options); - ValidateMutation(result); + ValidateRegexMutation(result); + } + + [TestMethod] + public void ShouldMutateReadOnlySpanRegexProperties() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + public class C { + [StringSyntax(StringSyntaxAttribute.Regex)] + public ReadOnlySpan RegexPattern => "^abc"; + } + """; + + var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(source); + var target = new StringMutator(); + var result = target.Mutate(expressionSyntax, semanticModel, _options); + + ValidateRegexMutation(result); } [TestMethod] @@ -268,7 +389,7 @@ public class C { var target = new StringMutator(); var result = target.Mutate(expressionSyntax, semanticModel, _options); - ValidateMutation(result); + ValidateRegexMutation(result); } [TestMethod] @@ -287,7 +408,7 @@ public class C { var target = new StringMutator(); var result = target.Mutate(expressionSyntax, semanticModel, _options); - ValidateMutation(result); + ValidateRegexMutation(result); } [TestMethod] @@ -321,7 +442,7 @@ public class C { var target = new StringMutator(); var result = target.Mutate(expressionSyntax, semanticModel, _options); - ValidateMutation(result); + ValidateRegexMutation(result); } [TestMethod] @@ -343,7 +464,7 @@ public void M() { var target = new StringMutator(); var result = target.Mutate(expressionSyntax, semanticModel, _options); - ValidateMutation(result); + ValidateRegexMutation(result); } [TestMethod] @@ -365,7 +486,7 @@ public void M() { var target = new StringMutator(); var result = target.Mutate(expressionSyntax, semanticModel, _options); - ValidateMutation(result); + ValidateRegexMutation(result); } [TestMethod] @@ -429,7 +550,7 @@ public static void Call([System.Diagnostics.CodeAnalysis.StringSyntax(System.Dia var target = new StringMutator(); var result = target.Mutate(expressionSyntax, semanticModel, _options); - ValidateMutation(result); + ValidateRegexMutation(result); } [TestMethod] @@ -450,7 +571,7 @@ public void M() { var target = new StringMutator(); var result = target.Mutate(expressionSyntax, semanticModel, _options); - ValidateMutation(result); + ValidateRegexMutation(result); } [TestMethod] @@ -467,6 +588,6 @@ public class C { var target = new StringMutator(); var result = target.Mutate(expressionSyntax, semanticModel, _options); - ValidateMutation(result); + ValidateRegexMutation(result); } } diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/StringMutatorTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/StringMutatorTests.cs index 059624235..c8545df0b 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/StringMutatorTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/StringMutatorTests.cs @@ -118,4 +118,40 @@ public Guid GetGuid(){ result.ShouldBeEmpty(); } + + [TestMethod] + [DataRow(@"""""u8", @"""Stryker was here!""u8")] + [DataRow(@"""foo""u8", @"""""u8")] + public void ShouldMutateUtf8StringLiteral(string original, string expected) + { + var syntaxTree = CSharpSyntaxTree.ParseText($"var test = {original};"); + + var literalExpression = syntaxTree.GetRoot().DescendantNodes().OfType().First(); + var mutator = new StringMutator(); + + var result = mutator.ApplyMutations(literalExpression, null, MutationLevel.Standard); + + var mutation = result.ShouldHaveSingleItem(); + + mutation.ReplacementNode.ShouldBeOfType() + .Token.Text.ShouldBe(expected); + mutation.DisplayName.ShouldBe("String mutation"); + } + + [TestMethod] + [DataRow(@"""""u8 + """"u8")] + [DataRow(@"""foo""u8 + """"u8")] + [DataRow(@" """"u8 + ""foo""u8")] + public void ShouldNotMutateConcatenatedUtf8StringLiteral(string original) + { + var syntaxTree = CSharpSyntaxTree.ParseText($"var test = {original};"); + + var literalExpression = syntaxTree.GetRoot().DescendantNodes().OfType().First(); + var mutator = new StringMutator(); + + var result = mutator.ApplyMutations(literalExpression, null, MutationLevel.Standard); + + result.ShouldBeEmpty(); + } + } diff --git a/src/Stryker.Core/Stryker.Core/Mutators/StringMutator.cs b/src/Stryker.Core/Stryker.Core/Mutators/StringMutator.cs index 8090f8219..770c25222 100644 --- a/src/Stryker.Core/Stryker.Core/Mutators/StringMutator.cs +++ b/src/Stryker.Core/Stryker.Core/Mutators/StringMutator.cs @@ -27,7 +27,8 @@ public class StringMutator : IMutator public IEnumerable Mutate(SyntaxNode node, SemanticModel semanticModel, IStrykerOptions options) { - if (node is LiteralExpressionSyntax tNode && node.IsKind(SyntaxKind.StringLiteralExpression)) + if (node is LiteralExpressionSyntax tNode && + node.Kind() is SyntaxKind.StringLiteralExpression or SyntaxKind.Utf8StringLiteralExpression) { // the node was of the expected type, so invoke the mutation method return ApplyMutations(tNode, semanticModel, options.MutationLevel); @@ -44,9 +45,10 @@ public IEnumerable ApplyMutations(LiteralExpressionSyntax node, Semant return ApplyRegexMutations(node, mutationLevel); } - if (OtherMutationLevel <= mutationLevel && !IsGuidType(node.Parent?.Parent?.Parent, semanticModel)) + if (OtherMutationLevel <= mutationLevel && !IsGuidType(node.Parent?.Parent?.Parent, semanticModel) && + ApplyStringMutations(node) is { } mutation) { - return [ApplyStringMutations(node)]; + return [mutation]; } return []; @@ -57,15 +59,35 @@ private static Mutation ApplyStringMutations(LiteralExpressionSyntax node) var currentValue = (string)node.Token.Value; var replacementValue = currentValue == "" ? "Stryker was here!" : ""; + + LiteralExpressionSyntax replacement; + if (node.IsKind(SyntaxKind.StringLiteralExpression)) + { + replacement = LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(replacementValue)); + } + else if (node.IsKind(SyntaxKind.Utf8StringLiteralExpression) && !InAdditionOperator(node)) + { + replacement = CreateUtf88String(node.GetLeadingTrivia(), replacementValue, node.GetTrailingTrivia()); + } + else + { + return null; + } + return new Mutation { OriginalNode = node, - ReplacementNode = LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(replacementValue)), + ReplacementNode = replacement, DisplayName = "String mutation", Type = Mutator.String }; } + // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-11.0/utf8-string-literals#addition-operator + private static bool InAdditionOperator(LiteralExpressionSyntax node) => node.AncestorsAndSelf() + .Any(a => a.IsKind(SyntaxKind.AddExpression) && a is BinaryExpressionSyntax bes && bes.DescendantNodes() + .OfType().All(b => b.IsKind(SyntaxKind.Utf8StringLiteralExpression))); + private IEnumerable ApplyRegexMutations(LiteralExpressionSyntax node, MutationLevel mutationLevel) { if (RegexMutationLevel > mutationLevel) @@ -117,8 +139,15 @@ private static bool IsRegexString(SyntaxNode node, SemanticModel semanticModel) case ArgumentSyntax { Parent.Parent: InvocationExpressionSyntax parentInvocation } argument when semanticModel?.GetOperation(parentInvocation) is IInvocationOperation invocationOp: var argumentOp = invocationOp.Arguments.SingleOrDefault(a => a.Syntax == argument); - - return argumentOp?.Parameter?.Type.Name is "String" && + return argumentOp?.Parameter?.Type is INamedTypeSymbol + { + TypeKind: TypeKind.Structure, IsRefLikeType: true, IsReadOnly: true, + IsValueType: true, MetadataName: "ReadOnlySpan`1", CanBeReferencedByName: true, + TypeArguments: [{ SpecialType: SpecialType.System_Char or SpecialType.System_Byte }] + } or + { + SpecialType: SpecialType.System_String + } && argumentOp.Parameter.GetAttributes().Any(IsRegexSyntaxAttribute); case FieldDeclarationSyntax field: return field.AttributeLists.Any(static a => a.Attributes.Any(IsRegexSyntaxAttribute)); @@ -240,4 +269,16 @@ private static bool IsCtorOfType(ImplicitObjectCreationExpressionSyntax ctor, Ty var ctorType = ti?.Type?.ToDisplayString(); return ctorType == type.Name || ctorType == type.FullName; } + + private static LiteralExpressionSyntax CreateUtf88String(SyntaxTriviaList leadingTrivia, string stringValue, + SyntaxTriviaList trailingTrivia) + { + const char QuoteCharacter = '"'; + var literal = Token(leadingTrivia, + SyntaxKind.Utf8StringLiteralToken, + $"{QuoteCharacter}{stringValue}{QuoteCharacter}u8", + stringValue, + trailingTrivia); + return LiteralExpression(SyntaxKind.Utf8StringLiteralExpression, literal); + } } From 680ea93b4fa1afe2413c849a9d76d08282c3e044 Mon Sep 17 00:00:00 2001 From: Pentiva Date: Thu, 7 Nov 2024 11:30:00 +0000 Subject: [PATCH 8/9] Added more tests --- .../RegexMutatorWithSemanticModelTests.cs | 163 ++++++++++++++---- .../Mutators/Strings/StringMutatorTests.cs | 14 +- .../Stryker.Core/Mutators/StringMutator.cs | 10 +- 3 files changed, 147 insertions(+), 40 deletions(-) diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/RegexMutatorWithSemanticModelTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/RegexMutatorWithSemanticModelTests.cs index 5269febaa..60d1308d0 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/RegexMutatorWithSemanticModelTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/RegexMutatorWithSemanticModelTests.cs @@ -24,31 +24,8 @@ public class RegexMutatorWithSemanticModelTests : TestBase private static (SemanticModel semanticModel, LiteralExpressionSyntax expression) CreateSemanticModelFromExpression(string input) { - // Parse the code into a syntax tree - var syntaxTree = CSharpSyntaxTree.ParseText(input); - - // Create a compilation that contains the syntax tree - var basePath = Path.GetDirectoryName(typeof(object).Assembly.Location)!; - var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); - var ros = MetadataReference.CreateFromFile(typeof(ReadOnlySpan<>).Assembly.Location); - var regex = MetadataReference.CreateFromFile(typeof(Regex).Assembly.Location); - var attribute = MetadataReference.CreateFromFile(Path.Combine(basePath, "System.Runtime.dll")); - var compilation = CSharpCompilation.Create("TestAssembly") - .WithOptions(new CSharpCompilationOptions(OutputKind - .DynamicallyLinkedLibrary)) - .AddReferences(mscorlib) - .AddReferences(ros) - .AddReferences(regex) - .AddReferences(attribute) - .AddSyntaxTrees(syntaxTree); - - // Get the semantic model from the compilation - var semanticModel = compilation.GetSemanticModel(syntaxTree); - - var expression = syntaxTree.GetRoot().DescendantNodes().OfType() - .First(a => a.IsKind(SyntaxKind.StringLiteralExpression)); - - return (semanticModel, expression); + var (semanticModel, expressions) = CreateSemanticModelFromExpressions(input); + return (semanticModel, expressions.First()); } private static (SemanticModel semanticModel, IEnumerable expression) @@ -60,23 +37,26 @@ private static (SemanticModel semanticModel, IEnumerable).Assembly.Location); var regex = MetadataReference.CreateFromFile(typeof(Regex).Assembly.Location); var attribute = MetadataReference.CreateFromFile(Path.Combine(basePath, "System.Runtime.dll")); var compilation = CSharpCompilation.Create("TestAssembly") .WithOptions(new CSharpCompilationOptions(OutputKind .DynamicallyLinkedLibrary)) .AddReferences(mscorlib) + .AddReferences(ros) .AddReferences(regex) .AddReferences(attribute) .AddSyntaxTrees(syntaxTree); // Get the semantic model from the compilation var semanticModel = compilation.GetSemanticModel(syntaxTree); + semanticModel.GetDiagnostics().Where(a => a.Severity == DiagnosticSeverity.Error).ShouldBeEmpty(); - var expression = syntaxTree.GetRoot().DescendantNodes().OfType() - .Where(a => a.IsKind(SyntaxKind.StringLiteralExpression)); + var expressions = syntaxTree.GetRoot().DescendantNodes().OfType() + .Where(a => a.IsKind(SyntaxKind.StringLiteralExpression)); - return (semanticModel, expression); + return (semanticModel, expressions); } private static void ValidateRegexMutation(IEnumerable result) @@ -181,6 +161,106 @@ public static void Call([StringSyntax(StringSyntaxAttribute.Regex)]string s) { ValidateRegexMutation(result); } + [TestMethod] + public void ShouldMutateCustomRegexMethodsNestedExpression() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + public class C { + public void M(string s2) { + Call(s2 ?? "^abc"); + } + + public static void Call([StringSyntax(StringSyntaxAttribute.Regex)]string s) { + + } + } + """; + + var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(source); + var target = new StringMutator(); + var result = target.Mutate(expressionSyntax, semanticModel, _options); + + ValidateRegexMutation(result); + } + + [TestMethod] + public void ShouldMutateCustomRegexMethodsNestedExpressions() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + public class C { + public void M(string s2) { + Call(true ? (s2 ?? "^abc") : s2); + } + + public static void Call([StringSyntax(StringSyntaxAttribute.Regex)]string s) { + + } + } + """; + + var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(source); + var target = new StringMutator(); + var result = target.Mutate(expressionSyntax, semanticModel, _options); + + ValidateRegexMutation(result); + } + + [TestMethod] + public void ShouldNotThrowOnWeirdCode() + { + // Sourced from CsharpCompilingProcess.cs LogEmitResult : Exact conditions for prior crash unknown + var source = """ + using System; + public class C { + public readonly ILogger _logger; + public void M(Diagnostic? err) { + _logger.LogDebug("{ErrorMessage}, {ErrorLocation}", err?.GetMessage() ?? "No message", err?.Location.ToString() ?? "Unknown filepath"); + } + } + public abstract class Diagnostic { + public abstract string GetMessage(IFormatProvider? formatProvider = null); + public abstract int Location { get; } + } + public static class Ext { + public static void LogDebug(this ILogger logger, string? message, params object?[] args) { } + } + public interface ILogger { } + """; + + var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpressions(source); + var target = new StringMutator(); + + foreach (var literalExpressionSyntax in expressionSyntax) + { + new Action(() => target.Mutate(literalExpressionSyntax, semanticModel, _options)).ShouldNotThrow(); + } + } + + [TestMethod] + public void ShouldMutateCustomRegexMethodsParams() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + public class C { + public void M() { + Call("^abc"); + } + + public static void Call([StringSyntax(StringSyntaxAttribute.Regex)]string s, params object[] obj) { + + } + } + """; + + var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(source); + var target = new StringMutator(); + var result = target.Mutate(expressionSyntax, semanticModel, _options); + + ValidateRegexMutation(result); + } + [TestMethod] public void ShouldMutateCustomRegexMethodReadonlySpan() { @@ -228,6 +308,28 @@ public static void Call([StringSyntax("Regex")]string s) { ValidateRegexMutation(result); } + [TestMethod] + public void ShouldNotMutateOtherMethodsWithParams() + { + var source = """ + public class C { + public void M() { + Call("^abc", "b", "c"); + } + + public static void Call(string s, params object[] obj) { + + } + } + """; + + var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(source); + var target = new StringMutator(); + var result = target.Mutate(expressionSyntax, semanticModel, _options); + + result.Where(a => a.Type == Mutator.Regex).ShouldBeEmpty(); + } + [TestMethod] public void ShouldNotMutateNonRegexParametersWithSimilarSyntax() { @@ -255,7 +357,7 @@ public static void Call([DefaultValue("Regex")]string s) { public void ShouldNotMutateBadNonRegexMethod() { var source = """ - using System.Diagnostics.CodeAnalysis; + using System; public class C { public void M() { Call("^abc"); @@ -265,6 +367,7 @@ public static void Call([StringSyntaxAttribute]string s) { } } + public class StringSyntaxAttributeAttribute : Attribute {} """; var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(source); @@ -359,6 +462,7 @@ public class C { public void ShouldMutateReadOnlySpanRegexProperties() { var source = """ + using System; using System.Diagnostics.CodeAnalysis; public class C { [StringSyntax(StringSyntaxAttribute.Regex)] @@ -433,6 +537,7 @@ public void ShouldMutateImplicitRegexConstructor() { var source = """ using System.Diagnostics.CodeAnalysis; + using System.Text.RegularExpressions; public class C { public static Regex RegexPattern = new("^abc"); } diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/StringMutatorTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/StringMutatorTests.cs index c8545df0b..ec505655e 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/StringMutatorTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/StringMutatorTests.cs @@ -141,17 +141,17 @@ public void ShouldMutateUtf8StringLiteral(string original, string expected) [TestMethod] [DataRow(@"""""u8 + """"u8")] [DataRow(@"""foo""u8 + """"u8")] - [DataRow(@" """"u8 + ""foo""u8")] + [DataRow(@"""""u8 + ""foo""u8")] + [DataRow(@"""foo""u8 + ""foo""u8 + ""foo""u8")] public void ShouldNotMutateConcatenatedUtf8StringLiteral(string original) { var syntaxTree = CSharpSyntaxTree.ParseText($"var test = {original};"); - var literalExpression = syntaxTree.GetRoot().DescendantNodes().OfType().First(); var mutator = new StringMutator(); - - var result = mutator.ApplyMutations(literalExpression, null, MutationLevel.Standard); - - result.ShouldBeEmpty(); + foreach (var literalExpression in syntaxTree.GetRoot().DescendantNodes().OfType()) + { + var result = mutator.ApplyMutations(literalExpression, null, MutationLevel.Standard); + result.ShouldBeEmpty(); + } } - } diff --git a/src/Stryker.Core/Stryker.Core/Mutators/StringMutator.cs b/src/Stryker.Core/Stryker.Core/Mutators/StringMutator.cs index 770c25222..cb528dfea 100644 --- a/src/Stryker.Core/Stryker.Core/Mutators/StringMutator.cs +++ b/src/Stryker.Core/Stryker.Core/Mutators/StringMutator.cs @@ -84,9 +84,11 @@ private static Mutation ApplyStringMutations(LiteralExpressionSyntax node) } // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-11.0/utf8-string-literals#addition-operator - private static bool InAdditionOperator(LiteralExpressionSyntax node) => node.AncestorsAndSelf() - .Any(a => a.IsKind(SyntaxKind.AddExpression) && a is BinaryExpressionSyntax bes && bes.DescendantNodes() - .OfType().All(b => b.IsKind(SyntaxKind.Utf8StringLiteralExpression))); + private static bool InAdditionOperator(LiteralExpressionSyntax node) => + node.AncestorsAndSelf() + .Any(static a => a.IsKind(SyntaxKind.AddExpression) && a.DescendantNodes() + .OfType() + .All(static b => b.IsKind(SyntaxKind.Utf8StringLiteralExpression))); private IEnumerable ApplyRegexMutations(LiteralExpressionSyntax node, MutationLevel mutationLevel) { @@ -146,7 +148,7 @@ private static bool IsRegexString(SyntaxNode node, SemanticModel semanticModel) TypeArguments: [{ SpecialType: SpecialType.System_Char or SpecialType.System_Byte }] } or { - SpecialType: SpecialType.System_String + SpecialType: SpecialType.System_String or SpecialType.System_Object } && argumentOp.Parameter.GetAttributes().Any(IsRegexSyntaxAttribute); case FieldDeclarationSyntax field: From 61f67296cf455db9b857f66a7c2c5ad16ea2618f Mon Sep 17 00:00:00 2001 From: Pentiva Date: Mon, 11 Nov 2024 21:14:54 +0000 Subject: [PATCH 9/9] chore: Update integration tests --- .../Validation/ValidationProject/ValidateStrykerResults.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/integrationtest/Validation/ValidationProject/ValidateStrykerResults.cs b/integrationtest/Validation/ValidationProject/ValidateStrykerResults.cs index 788657b66..291e9b6a9 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: 601, ignored: 247, survived: 4, killed: 9, timeout: 2, nocoverage: 308); + CheckReportMutants(report, total: 603, ignored: 249, 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: 601, ignored: 105, survived: 5, killed: 11, timeout: 2, nocoverage: 447); + CheckReportMutants(report, total: 603, ignored: 106, survived: 5, killed: 11, timeout: 2, nocoverage: 448); CheckReportTestCounts(report, total: 21); } @@ -141,7 +141,7 @@ public async Task CSharp_NetCore_SolutionRun() var report = await JsonReportSerialization.DeserializeJsonReportAsync(strykerRunOutput); - CheckReportMutants(report, total: 601, ignored: 247, survived: 4, killed: 9, timeout: 2, nocoverage: 308); + CheckReportMutants(report, total: 603, ignored: 249, survived: 4, killed: 9, timeout: 2, nocoverage: 308); CheckReportTestCounts(report, total: 23); }