diff --git a/docs/mutations.md b/docs/mutations.md index 8afe59879..db7196f88 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 d8c924864..70ea88b98 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, fields, and properties that are annotated with `[StringSyntax(StringSyntaxAttribute.Regex)]` on NET7+, and just the Regex constructor on prior targets. ## 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)` | 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); } diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutants/CsharpMutantOrchestratorTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutants/CsharpMutantOrchestratorTests.cs index 65b752315..392ed82fc 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Mutants/CsharpMutantOrchestratorTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Mutants/CsharpMutantOrchestratorTests.cs @@ -1602,7 +1602,7 @@ int TestMethod() } [TestMethod] - public void ShouldMutatetringsInSwitchExpression() + public void ShouldMutateStringsInSwitchExpression() { var source = @"string TestMethod() { diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/RegexMutatorTest.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/RegexMutatorTest.cs deleted file mode 100644 index a29c88efe..000000000 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/RegexMutatorTest.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Linq; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Shouldly; -using Stryker.Abstractions.Mutators; -using Stryker.Core.Mutators; - -namespace Stryker.Core.UnitTest.Mutators; - -[TestClass] -public class RegexMutatorTest : TestBase -{ - [TestMethod] - public void ShouldBeMutationLevelAdvanced() - { - var target = new RegexMutator(); - target.MutationLevel.ShouldBe(MutationLevel.Advanced); - } - - [TestMethod] - public void ShouldMutateStringLiteralInRegexConstructor() - { - var objectCreationExpression = SyntaxFactory.ParseExpression("new Regex(@\"^abc\")") as ObjectCreationExpressionSyntax; - var target = new RegexMutator(); - - var result = target.ApplyMutations(objectCreationExpression, null); - - var mutation = result.ShouldHaveSingleItem(); - - mutation.DisplayName.ShouldBe("Regex anchor removal mutation"); - var replacement = mutation.ReplacementNode.ShouldBeOfType(); - replacement.Token.ValueText.ShouldBe("abc"); - } - - [TestMethod] - public void ShouldMutateStringLiteralInRegexConstructorWithFullName() - { - var objectCreationExpression = SyntaxFactory.ParseExpression("new System.Text.RegularExpressions.Regex(@\"^abc\")") as ObjectCreationExpressionSyntax; - var target = new RegexMutator(); - - var result = target.ApplyMutations(objectCreationExpression, null); - - var mutation = result.ShouldHaveSingleItem(); - - mutation.DisplayName.ShouldBe("Regex anchor removal mutation"); - var replacement = mutation.ReplacementNode.ShouldBeOfType(); - 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); - - result.ShouldBeEmpty(); - } - - [TestMethod] - public void ShouldMutateStringLiteralMultipleTimes() - { - var objectCreationExpression = SyntaxFactory.ParseExpression("new Regex(@\"^abc$\")") as ObjectCreationExpressionSyntax; - var target = new RegexMutator(); - - var result = target.ApplyMutations(objectCreationExpression, null); - - result.Count().ShouldBe(2); - result.ShouldAllBe(mutant => mutant.DisplayName == "Regex anchor removal mutation"); - var first = result.First().ReplacementNode.ShouldBeOfType(); - var last = result.Last().ReplacementNode.ShouldBeOfType(); - first.Token.ValueText.ShouldBe("abc$"); - last.Token.ValueText.ShouldBe("^abc"); - } - - [TestMethod] - public void ShouldMutateStringLiteralAsNamedArgumentPatternInRegexConstructor() - { - var objectCreationExpression = SyntaxFactory.ParseExpression("new Regex(options: RegexOptions.None, pattern: @\"^abc\")") as ObjectCreationExpressionSyntax; - var target = new RegexMutator(); - - var result = target.ApplyMutations(objectCreationExpression, null); - - var mutation = result.ShouldHaveSingleItem(); - - mutation.DisplayName.ShouldBe("Regex anchor removal mutation"); - var replacement = mutation.ReplacementNode.ShouldBeOfType(); - replacement.Token.ValueText.ShouldBe("abc"); - } -} diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/RegexMutatorTest.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/RegexMutatorTest.cs new file mode 100644 index 000000000..2453e5718 --- /dev/null +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/RegexMutatorTest.cs @@ -0,0 +1,121 @@ +using System.Linq; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using Stryker.Abstractions.Mutators; +using Stryker.Core.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() + { + var target = new StringMutator(); + 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() + { + var literalExpression = ParseExpression("new Regex(@\"^abc\")"); + var target = new StringMutator(); + + var result = target.ApplyMutations(literalExpression, null, MutationLevel.Advanced); + + var mutation = result.ShouldHaveSingleItem(); + + mutation.DisplayName.ShouldBe("Regex anchor removal mutation"); + var replacement = mutation.ReplacementNode.ShouldBeOfType(); + replacement.Token.ValueText.ShouldBe("abc"); + } + + [TestMethod] + public void ShouldMutateStringLiteralInRegexConstructorWithFullName() + { + var literalExpression = ParseExpression("new System.Text.RegularExpressions.Regex(@\"^abc\")"); + var target = new StringMutator(); + + var result = target.ApplyMutations(literalExpression, null, MutationLevel.Advanced); + + var mutation = result.ShouldHaveSingleItem(); + + mutation.DisplayName.ShouldBe("Regex anchor removal mutation"); + var replacement = mutation.ReplacementNode.ShouldBeOfType(); + replacement.Token.ValueText.ShouldBe("abc"); + } + + [TestMethod] + public void ShouldNotMutateStringLiteralInOtherConstructor() + { + 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(literalExpression, null, MutationLevel.Standard); + + result.Where(a => a.Type == Mutator.Regex).ShouldBeEmpty(); + } + + [TestMethod] + public void ShouldMutateStringLiteralMultipleTimes() + { + var literalExpression = ParseExpression("new Regex(@\"^abc$\")"); + var target = new StringMutator(); + + var result = target.ApplyMutations(literalExpression, null, MutationLevel.Advanced); + + result.Count().ShouldBe(2); + result.ShouldAllBe(mutant => mutant.DisplayName == "Regex anchor removal mutation"); + var first = result.First().ReplacementNode.ShouldBeOfType(); + var last = result.Last().ReplacementNode.ShouldBeOfType(); + first.Token.ValueText.ShouldBe("abc$"); + last.Token.ValueText.ShouldBe("^abc"); + } + + [TestMethod] + public void ShouldMutateStringLiteralAsNamedArgumentPatternInRegexConstructor() + { + var literalExpression = ParseExpression("new Regex(options: RegexOptions.None, pattern: @\"^abc\")"); + var target = new StringMutator(); + + var result = target.ApplyMutations(literalExpression, null, MutationLevel.Advanced); + + var mutation = result.ShouldHaveSingleItem(); + + mutation.DisplayName.ShouldBe("Regex anchor removal mutation"); + var replacement = mutation.ReplacementNode.ShouldBeOfType(); + replacement.Token.ValueText.ShouldBe("abc"); + } +} 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..60d1308d0 --- /dev/null +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/RegexMutatorWithSemanticModelTests.cs @@ -0,0 +1,698 @@ +using System; +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) + { + var (semanticModel, expressions) = CreateSemanticModelFromExpressions(input); + return (semanticModel, expressions.First()); + } + + 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 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); + semanticModel.GetDiagnostics().Where(a => a.Severity == DiagnosticSeverity.Error).ShouldBeEmpty(); + + var expressions = syntaxTree.GetRoot().DescendantNodes().OfType() + .Where(a => a.IsKind(SyntaxKind.StringLiteralExpression)); + + return (semanticModel, expressions); + } + + private static void ValidateRegexMutation(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); + + 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] + 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); + + 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() + { + 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] + public void ShouldMutateCustomRegexMethodsWithManualSyntax() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + public class C { + public void M() { + Call("^abc"); + } + + public static void Call([StringSyntax("Regex")]string s) { + + } + } + """; + + var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(source); + var target = new StringMutator(); + var result = target.Mutate(expressionSyntax, semanticModel, _options); + + 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() + { + var source = """ + using System.ComponentModel; + public class C { + public void M() { + Call("^abc"); + } + + public static void Call([DefaultValue("Regex")]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 ShouldNotMutateBadNonRegexMethod() + { + var source = """ + using System; + public class C { + public void M() { + Call("^abc"); + } + + public static void Call([StringSyntaxAttribute]string s) { + + } + } + public class StringSyntaxAttributeAttribute : Attribute {} + """; + + 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([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) { + + } + } + """; + + 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); + + ValidateRegexMutation(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); + + ValidateRegexMutation(result); + } + + [TestMethod] + public void ShouldMutateReadOnlySpanRegexProperties() + { + var source = """ + using System; + 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] + public void ShouldMutateRegexFieldsWithMultipleAttributes() + { + var source = """ + using System.ComponentModel; + using System.Diagnostics.CodeAnalysis; + public class C { + [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); + + ValidateRegexMutation(result); + } + + [TestMethod] + public void ShouldMutateRegexPropertiesWithMultipleAttributes() + { + var source = """ + using System.ComponentModel; + using System.Diagnostics.CodeAnalysis; + public class C { + [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); + + ValidateRegexMutation(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 ShouldMutateImplicitRegexConstructor() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + using System.Text.RegularExpressions; + 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); + + ValidateRegexMutation(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); + + ValidateRegexMutation(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); + + ValidateRegexMutation(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); + + ValidateRegexMutation(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); + + ValidateRegexMutation(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); + + ValidateRegexMutation(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 67% rename from src/Stryker.Core/Stryker.Core.UnitTest/Mutators/StringMutatorTests.cs rename to src/Stryker.Core/Stryker.Core.UnitTest/Mutators/Strings/StringMutatorTests.cs index 73d827274..ec505655e 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 @@ -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,8 +114,44 @@ 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(); } + + [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")] + [DataRow(@"""foo""u8 + ""foo""u8 + ""foo""u8")] + public void ShouldNotMutateConcatenatedUtf8StringLiteral(string original) + { + var syntaxTree = CSharpSyntaxTree.ParseText($"var test = {original};"); + + var mutator = new StringMutator(); + 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/Mutants/CsharpMutantOrchestrator.cs b/src/Stryker.Core/Stryker.Core/Mutants/CsharpMutantOrchestrator.cs index 09b193245..f1c2e7e3d 100644 --- a/src/Stryker.Core/Stryker.Core/Mutants/CsharpMutantOrchestrator.cs +++ b/src/Stryker.Core/Stryker.Core/Mutants/CsharpMutantOrchestrator.cs @@ -111,7 +111,6 @@ private static List DefaultMutatorList() => new ObjectCreationMutator(), new ArrayCreationMutator(), new StatementMutator(), - new RegexMutator(), new NullCoalescingExpressionMutator(), new MathMutator(), new SwitchExpressionMutator(), 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 3e33c8feb..000000000 --- a/src/Stryker.Core/Stryker.Core/Mutators/RegexMutator.cs +++ /dev/null @@ -1,72 +0,0 @@ -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 -{ - private const string PatternArgumentName = "pattern"; - private ILogger Logger { get; } - - public override MutationLevel MutationLevel => MutationLevel.Advanced; - - public RegexMutator() - { - Logger = ApplicationLogging.LoggerFactory.CreateLogger(); - } - - public override IEnumerable ApplyMutations(ObjectCreationExpressionSyntax node, - SemanticModel semanticModel) - { - var name = node.Type.ToString(); - 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; - - 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; - } - - 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/StringMutator.cs b/src/Stryker.Core/Stryker.Core/Mutators/StringMutator.cs index 95512fe42..cb528dfea 100644 --- a/src/Stryker.Core/Stryker.Core/Mutators/StringMutator.cs +++ b/src/Stryker.Core/Stryker.Core/Mutators/StringMutator.cs @@ -1,46 +1,260 @@ +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; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; 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 MutationLevel RegexMutationLevel => MutationLevel.Advanced; + + public MutationLevel OtherMutationLevel => MutationLevel.Standard; + + public IEnumerable Mutate(SyntaxNode node, SemanticModel semanticModel, IStrykerOptions options) + { + 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); + } + + return []; + } + + public IEnumerable ApplyMutations(LiteralExpressionSyntax node, SemanticModel semanticModel, + MutationLevel mutationLevel) + { + if (IsRegexString(node, semanticModel)) + { + return ApplyRegexMutations(node, mutationLevel); + } + + if (OtherMutationLevel <= mutationLevel && !IsGuidType(node.Parent?.Parent?.Parent, semanticModel) && + ApplyStringMutations(node) is { } mutation) + { + return [mutation]; + } + + return []; + } + + 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; + } - public override IEnumerable ApplyMutations(LiteralExpressionSyntax node, SemanticModel semanticModel) + return new Mutation + { + OriginalNode = node, + 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(static a => a.IsKind(SyntaxKind.AddExpression) && a.DescendantNodes() + .OfType() + .All(static b => b.IsKind(SyntaxKind.Utf8StringLiteralExpression))); + + private IEnumerable ApplyRegexMutations(LiteralExpressionSyntax node, MutationLevel mutationLevel) { - // Get objectCreationSyntax to check if it contains a regex type. - var root = node.Parent?.Parent?.Parent; + if (RegexMutationLevel > mutationLevel) + { + yield break; + } + + var currentValue = node.Token.ValueText; + var regexMutantOrchestrator = new RegexMutantOrchestrator(currentValue); + var replacementValues = regexMutantOrchestrator.Mutate(); - if (!IsSpecialType(root) && IsStringLiteral(node)) + 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 + ReplacementNode = + LiteralExpression(SyntaxKind.StringLiteralExpression, + Literal(regexMutation.ReplacementPattern)), + DisplayName = regexMutation.DisplayName, + Type = Mutator.Regex, + Description = regexMutation.Description }; } } - 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 { 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 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 or SpecialType.System_Object + } && + 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 IAssignmentOperation + { + 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; + } + } + + return false; } - private static bool IsSpecialType(SyntaxNode root) => root switch + private static bool IsRegexSyntaxAttribute(AttributeSyntax attributeSyntax) => + IsStringSyntaxAttribute(attributeSyntax.Name) && + attributeSyntax.ArgumentList?.Arguments is [var arg] && + (arg.Expression is + LiteralExpressionSyntax { Token.ValueText: "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 IsGuidType(SyntaxNode root, SemanticModel semanticModel) => root switch { - ObjectCreationExpressionSyntax ctor => IsCtorOfType(ctor, typeof(Regex)) || IsCtorOfType(ctor, typeof(Guid)), + ObjectCreationExpressionSyntax ctor => IsCtorOfType(ctor, typeof(Guid)), + ImplicitObjectCreationExpressionSyntax ctor => IsCtorOfType(ctor, typeof(Guid), semanticModel), _ => false }; @@ -49,4 +263,24 @@ 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; + } + + 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); + } }