From 3f6e3ad8dc13ddb0aa244178321788ddaee209dc Mon Sep 17 00:00:00 2001 From: Myoxocephalus Date: Fri, 18 Oct 2024 12:21:16 +0200 Subject: [PATCH 1/3] feat: Add string mutators for LastIndexOf and IndexOf. (#3054) Added string mutators. --- docs/mutations.md | 2 ++ .../Mutators/StringMethodMutatorTests.cs | 2 ++ src/Stryker.Core/Stryker.Core/Mutators/StringMethodMutator.cs | 4 +++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/mutations.md b/docs/mutations.md index 16ab045dd..4fe129a1b 100644 --- a/docs/mutations.md +++ b/docs/mutations.md @@ -173,6 +173,8 @@ Do you have a suggestion for a (new) mutator? Feel free to create an [issue](htt | `ElementAt()` | `'\0'` | | `ElementAtOrDefault()` | `'\0'` | | `EndsWith()` | `StartsWith()` | +| `IndexOf()` | `LastIndexOf()` | +| `LastIndexOf()` | `IndexOf()` | | `PadLeft()` | `PadRight()` | | `PadRight()` | `PadLeft()` | | `StartsWith()` | `EndsWith()` | diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/StringMethodMutatorTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/StringMethodMutatorTests.cs index 477182126..9739cc82a 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/StringMethodMutatorTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/StringMethodMutatorTests.cs @@ -59,6 +59,8 @@ void TestMethod() { "String Method Mutation (Replace ToLowerInvariant() with ToUpperInvariant())")] [DataRow("testString.PadLeft(10)", "PadRight", "String Method Mutation (Replace PadLeft() with PadRight())")] [DataRow("testString.PadRight(10)", "PadLeft", "String Method Mutation (Replace PadRight() with PadLeft())")] + [DataRow("testString.LastIndexOf(c)", "IndexOf", "String Method Mutation (Replace LastIndexOf() with IndexOf())")] + [DataRow("testString.IndexOf(c)", "LastIndexOf", "String Method Mutation (Replace IndexOf() with LastIndexOf())")] public void ShouldMutateStringMethods(string expression, string mutatedMethod, string expectedDisplayName) { var (semanticModel, expressionSyntax) = CreateSemanticModelFromExpression(expression); diff --git a/src/Stryker.Core/Stryker.Core/Mutators/StringMethodMutator.cs b/src/Stryker.Core/Stryker.Core/Mutators/StringMethodMutator.cs index 815866082..99367a780 100644 --- a/src/Stryker.Core/Stryker.Core/Mutators/StringMethodMutator.cs +++ b/src/Stryker.Core/Stryker.Core/Mutators/StringMethodMutator.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -63,6 +63,8 @@ private static string GetReplacement(string identifier) => "ToLowerInvariant" => "ToUpperInvariant", "PadLeft" => "PadRight", "PadRight" => "PadLeft", + "IndexOf" => "LastIndexOf", + "LastIndexOf" => "IndexOf", _ => null }; From 48122758fee0a3b720f213fe363a3c6603bf5c45 Mon Sep 17 00:00:00 2001 From: Java Roy Date: Fri, 18 Oct 2024 13:54:59 +0100 Subject: [PATCH 2/3] feat(Regex): Add more regex mutations (#3060) * feat(Regex): Add more regex mutations * chore(Tests): Improve modified tests --- docs/regex-mutations.md | 108 +++++++++ .../CharacterClassChildRemovalMutatorTests.cs | 229 ++++++++++++++++++ .../CharacterClassRangeMutatorTests.cs | 209 ++++++++++++++++ ...racterClassShorthandAnyCharMutatorTests.cs | 72 ++++++ ...ClassShorthandNullificationMutatorTests.cs | 72 ++++++ .../CharacterClassToAnyCharMutatorTests.cs | 82 +++++++ .../Mutators/GroupToNcGroupMutatorTests.cs | 70 ++++++ ...QuantifierReluctantAdditionMutatorTests.cs | 87 +++++++ .../Mutators/QuantifierShortMutatorTests.cs | 133 ++++++++++ .../Mutators/TestHelpers.cs | 18 ++ .../UnicodeCharClassNegationMutatorTests.cs | 142 +++++++++++ .../RegexMutantOrchestratorTest.cs | 49 +++- .../CharacterClassChildRemovalMutator.cs | 59 +++++ .../Mutators/CharacterClassRangeMutator.cs | 84 +++++++ .../CharacterClassShorthandAnyCharMutator.cs | 29 +++ ...acterClassShorthandNullificationMutator.cs | 23 ++ .../CharacterClassToAnyCharMutator.cs | 37 +++ .../Mutators/GroupToNcGroupMutator.cs | 23 ++ .../Mutators/IRegexMutator.cs | 6 +- .../QuantifierReluctantAdditionMutator.cs | 27 +++ .../Mutators/QuantifierShortMutator.cs | 59 +++++ .../Mutators/RegexMutatorBase.cs | 6 +- .../UnicodeCharClassNegationMutator.cs | 23 ++ .../RegexMutantOrchestrator.cs | 81 ++++--- 24 files changed, 1679 insertions(+), 49 deletions(-) create mode 100644 src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/CharacterClassChildRemovalMutatorTests.cs create mode 100644 src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/CharacterClassRangeMutatorTests.cs create mode 100644 src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/CharacterClassShorthandAnyCharMutatorTests.cs create mode 100644 src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/CharacterClassShorthandNullificationMutatorTests.cs create mode 100644 src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/CharacterClassToAnyCharMutatorTests.cs create mode 100644 src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/GroupToNcGroupMutatorTests.cs create mode 100644 src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/QuantifierReluctantAdditionMutatorTests.cs create mode 100644 src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/QuantifierShortMutatorTests.cs create mode 100644 src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/TestHelpers.cs create mode 100644 src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/UnicodeCharClassNegationMutatorTests.cs create mode 100644 src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/CharacterClassChildRemovalMutator.cs create mode 100644 src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/CharacterClassRangeMutator.cs create mode 100644 src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/CharacterClassShorthandAnyCharMutator.cs create mode 100644 src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/CharacterClassShorthandNullificationMutator.cs create mode 100644 src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/CharacterClassToAnyCharMutator.cs create mode 100644 src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/GroupToNcGroupMutator.cs create mode 100644 src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/QuantifierReluctantAdditionMutator.cs create mode 100644 src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/QuantifierShortMutator.cs create mode 100644 src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/UnicodeCharClassNegationMutator.cs diff --git a/docs/regex-mutations.md b/docs/regex-mutations.md index b63eb41cb..d8c924864 100644 --- a/docs/regex-mutations.md +++ b/docs/regex-mutations.md @@ -53,3 +53,111 @@ Stryker supports a variety of regular expression mutators, which are listed belo | `(?!abc)` | `(?=abc)` | | `(?<=abc)` | `(? a.ReplacementPattern).ToArray().ShouldBeEquivalentTo(expected); + } + + [TestMethod] + [DataRow("[A-Z]")] + [DataRow("[A]")] + [DataRow(@"[\u1234]")] + public void DoesNotRemoveSingleItemClasses(string pattern) + { + // Act + var result = TestHelpers.ParseAndMutate(pattern, new CharacterClassChildRemovalMutator()); + + // Assert + result.ShouldBeEmpty(); + } + + [TestMethod] + public void ShouldRemoveEachChildOfTheCharacterClass() + { + // Arrange + RegexNode a; + RegexNode b; + RegexNode c; + + var characterClassNode = new CharacterClassNode(new CharacterClassCharacterSetNode([ + a = new CharacterNode('a'), b = new CharacterNode('b'), c = new CharacterNode('c') + ]), false); + + var childNodes = new List { characterClassNode, new CharacterNode('a') }; + var rootNode = new ConcatenationNode(childNodes); + var target = new CharacterClassChildRemovalMutator(); + + // Act + var result = target.ApplyMutations(characterClassNode, rootNode); + + // Assert + var regexMutations = result as RegexMutation[] ?? result.ToArray(); + regexMutations.Length.ShouldBe(3); + regexMutations.ElementAt(0).OriginalNode.ShouldBe(a); + regexMutations.ElementAt(0).ReplacementNode.ShouldBeNull(); + regexMutations.ElementAt(0).ReplacementPattern.ShouldBe("[bc]a"); + regexMutations.ElementAt(0).DisplayName.ShouldBe("Regex character class child removal"); + + regexMutations.ElementAt(0) + .Description.ShouldBe("""Removed child "a" from character class "[abc]" at offset 0."""); + + regexMutations.ElementAt(1).OriginalNode.ShouldBe(b); + regexMutations.ElementAt(1).ReplacementNode.ShouldBeNull(); + regexMutations.ElementAt(1).ReplacementPattern.ShouldBe("[ac]a"); + regexMutations.ElementAt(1).DisplayName.ShouldBe("Regex character class child removal"); + + regexMutations.ElementAt(1) + .Description.ShouldBe("""Removed child "b" from character class "[abc]" at offset 0."""); + + regexMutations.ElementAt(2).OriginalNode.ShouldBe(c); + regexMutations.ElementAt(2).ReplacementNode.ShouldBeNull(); + regexMutations.ElementAt(2).ReplacementPattern.ShouldBe("[ab]a"); + regexMutations.ElementAt(2).DisplayName.ShouldBe("Regex character class child removal"); + + regexMutations.ElementAt(2) + .Description.ShouldBe("""Removed child "c" from character class "[abc]" at offset 0."""); + } + + [TestMethod] + public void ShouldRemoveEachChildOfTheCharacterClassAndTheSubstitution() + { + // Arrange + RegexNode a; + RegexNode b; + RegexNode c; + CharacterClassNode sub; + + var characterClassNode = new CharacterClassNode(new CharacterClassCharacterSetNode([ + a = new CharacterNode('a'), b = new CharacterNode('b'), c = new CharacterNode('c') + ]), sub = new CharacterClassNode(new CharacterClassCharacterSetNode([new CharacterNode('b')]), false), false); + + var childNodes = new List { characterClassNode, new CharacterNode('a') }; + var rootNode = new ConcatenationNode(childNodes); + var target = new CharacterClassChildRemovalMutator(); + + // Act + var result = target.ApplyMutations(characterClassNode, rootNode); + + // Assert + var regexMutations = result as RegexMutation[] ?? result.ToArray(); + regexMutations.Length.ShouldBe(4); + regexMutations.ElementAt(0).OriginalNode.ShouldBe(sub); + regexMutations.ElementAt(0).ReplacementNode.ShouldBeNull(); + regexMutations.ElementAt(0).ReplacementPattern.ShouldBe("[abc]a"); + regexMutations.ElementAt(0).DisplayName.ShouldBe("Regex character class subtraction removal"); + + regexMutations.ElementAt(0) + .Description.ShouldBe("""Character Class Subtraction "-[b]" was removed at offset 4."""); + + regexMutations.ElementAt(1).OriginalNode.ShouldBe(a); + regexMutations.ElementAt(1).ReplacementNode.ShouldBeNull(); + regexMutations.ElementAt(1).ReplacementPattern.ShouldBe("[bc-[b]]a"); + regexMutations.ElementAt(1).DisplayName.ShouldBe("Regex character class child removal"); + + regexMutations.ElementAt(1) + .Description.ShouldBe("""Removed child "a" from character class "[abc-[b]]" at offset 0."""); + + regexMutations.ElementAt(2).OriginalNode.ShouldBe(b); + regexMutations.ElementAt(2).ReplacementNode.ShouldBeNull(); + regexMutations.ElementAt(2).ReplacementPattern.ShouldBe("[ac-[b]]a"); + regexMutations.ElementAt(2).DisplayName.ShouldBe("Regex character class child removal"); + + regexMutations.ElementAt(2) + .Description.ShouldBe("""Removed child "b" from character class "[abc-[b]]" at offset 0."""); + + regexMutations.ElementAt(3).OriginalNode.ShouldBe(c); + regexMutations.ElementAt(3).ReplacementNode.ShouldBeNull(); + regexMutations.ElementAt(3).ReplacementPattern.ShouldBe("[ab-[b]]a"); + regexMutations.ElementAt(3).DisplayName.ShouldBe("Regex character class child removal"); + + regexMutations.ElementAt(3) + .Description.ShouldBe("""Removed child "c" from character class "[abc-[b]]" at offset 0."""); + } + + [TestMethod] + public void ShouldRemoveOnlyChildOfTheCharacterClassAndTheSubstitution() + { + // Arrange + var sub = new CharacterClassNode(new CharacterClassCharacterSetNode([new CharacterNode('b')]), false); + + var characterClassNode = + new CharacterClassNode(new CharacterClassCharacterSetNode([new CharacterNode('a')]), sub, false); + + var childNodes = new List { characterClassNode, new CharacterNode('a') }; + var rootNode = new ConcatenationNode(childNodes); + var target = new CharacterClassChildRemovalMutator(); + + // Act + var result = target.ApplyMutations(characterClassNode, rootNode); + + // Assert + var regexMutations = result as RegexMutation[] ?? result.ToArray(); + regexMutations.Length.ShouldBe(2); + regexMutations.ElementAt(0).OriginalNode.ShouldBe(sub); + regexMutations.ElementAt(0).ReplacementNode.ShouldBeNull(); + regexMutations.ElementAt(0).ReplacementPattern.ShouldBe("[a]a"); + regexMutations.ElementAt(0).DisplayName.ShouldBe("Regex character class subtraction removal"); + + regexMutations.ElementAt(0) + .Description.ShouldBe("""Character Class Subtraction "-[b]" was removed at offset 2."""); + + regexMutations.ElementAt(1).OriginalNode.ShouldBe(characterClassNode); + regexMutations.ElementAt(1).ReplacementNode.ToString().ShouldBe(sub.ToString()); + regexMutations.ElementAt(1).ReplacementPattern.ShouldBe("[b]a"); + regexMutations.ElementAt(1).DisplayName.ShouldBe("Regex character class subtraction replacement"); + + regexMutations.ElementAt(1) + .Description + .ShouldBe("""Character Class "[a-[b]]" was replace with its subtraction "[b]" at offset 0."""); + } + + [TestMethod] + public void ShouldNotMutateNonCharacterClassNode() + { + // Arrange + var characterNode = new CharacterNode('a'); + var rootNode = new ConcatenationNode(characterNode); + var target = new CharacterClassChildRemovalMutator(); + + // Act + var result = target.Mutate(characterNode, rootNode); + + // Assert + result.ShouldBeEmpty(); + } + + [TestMethod] + public void ShouldNotMutateCharacterClassWithSingleChild() + { + // Arrange + var characterClassNode = + new CharacterClassNode(new CharacterClassCharacterSetNode([new CharacterNode('a')]), false); + var rootNode = new ConcatenationNode(characterClassNode); + var target = new CharacterClassChildRemovalMutator(); + + // Act + var result = target.Mutate(characterClassNode, rootNode); + + // Assert + result.ShouldBeEmpty(); + } +} + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +file class CharacterClassRemoveChildAttribute : DataRowAttribute +{ + /// + public CharacterClassRemoveChildAttribute(string pattern, string[] expected) : base(pattern, expected) => + DisplayName = $"Removes children of Character Classes {pattern}"; +} diff --git a/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/CharacterClassRangeMutatorTests.cs b/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/CharacterClassRangeMutatorTests.cs new file mode 100644 index 000000000..448112ba2 --- /dev/null +++ b/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/CharacterClassRangeMutatorTests.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using Stryker.Regex.Parser.Nodes.CharacterClass; +using Stryker.Regex.Parser.Nodes; +using Stryker.RegexMutators.Mutators; + +namespace Stryker.RegexMutators.UnitTest.Mutators; + +[TestClass] +public sealed class CharacterClassRangeMutatorTests +{ + [DataTestMethod("Character Class Modify Range")] + [CharacterClassModifyRange("[b-y][B-Y][1-8]", [ + // [b-y] -> [a-y] or [c-y] or [b-z] or [b-x] + "[a-y][B-Y][1-8]", "[c-y][B-Y][1-8]", "[b-x][B-Y][1-8]", "[b-z][B-Y][1-8]", + // [B-Y] -> [A-Y] OR [C-Y] OR [B-Z] OR [B-X] + "[b-y][A-Y][1-8]", "[b-y][C-Y][1-8]", "[b-y][B-X][1-8]", "[b-y][B-Z][1-8]", + // [1-8] -> [0-8] OR [2-8] OR [1-9] OR [1-7] + "[b-y][B-Y][0-8]", "[b-y][B-Y][2-8]", "[b-y][B-Y][1-7]", "[b-y][B-Y][1-9]" + ])] + [CharacterClassModifyRange("[a-y][A-Y][0-8]", [ + // [a-y] -> [b-y] or [a-z] or [a-x] + "[b-y][A-Y][0-8]", "[a-x][A-Y][0-8]", "[a-z][A-Y][0-8]", + // [A-Y] -> [B-Y] OR [A-Z] OR [A-X] + "[a-y][B-Y][0-8]", "[a-y][A-X][0-8]", "[a-y][A-Z][0-8]", + // [0-8] -> [1-8] OR [0-9] OR [0-7] + "[a-y][A-Y][1-8]", "[a-y][A-Y][0-7]", "[a-y][A-Y][0-9]" + ])] + [CharacterClassModifyRange("[b-z][B-Z][1-9]", [ + // [b-z] -> [a-z] or [c-z] or [b-y] + "[a-z][B-Z][1-9]", "[c-z][B-Z][1-9]", "[b-y][B-Z][1-9]", + // [B-Z] -> [A-Z] OR [C-Z] OR [B-Y] + "[b-z][A-Z][1-9]", "[b-z][C-Z][1-9]", "[b-z][B-Y][1-9]", + // [1-9] -> [0-9] OR [2-9] OR [1-8] + "[b-z][B-Z][0-9]", "[b-z][B-Z][2-9]", "[b-z][B-Z][1-8]" + ])] + [CharacterClassModifyRange("[a-z][A-Z][0-9]", [ + // [a-z] -> [b-z] or [a-y] + "[b-z][A-Z][0-9]", "[a-y][A-Z][0-9]", + // [A-Z] -> [B-Z] OR [A-Y] + "[a-z][B-Z][0-9]", "[a-z][A-Y][0-9]", + // [0-9] -> [1-9] OR [0-8] + "[a-z][A-Z][1-9]", "[a-z][A-Z][0-8]" + ])] + [CharacterClassModifyRange("[b-b][B-B][1-1]", [ + // [b-b] -> [a-b] or [b-c] + "[a-b][B-B][1-1]", "[b-c][B-B][1-1]", + // [B-B] -> [A-B] OR [B-C] + "[b-b][A-B][1-1]", "[b-b][B-C][1-1]", + // [1-1] -> [0-1] OR [1-2] + "[b-b][B-B][0-1]", "[b-b][B-B][1-2]" + ])] + [CharacterClassModifyRange("[a-a][A-A][0-0]", [ + // [a-a] -> [a-b] + "[a-b][A-A][0-0]", + // [A-A] -> [A-B] + "[a-a][A-B][0-0]", + // [0-0] -> [0-1] + "[a-a][A-A][0-1]" + ])] + [CharacterClassModifyRange("[z-z][Z-Z][9-9]", [ + // [z-z] -> [y-z] + "[y-z][Z-Z][9-9]", + // [Z-Z] -> [Y-Z] + "[z-z][Y-Z][9-9]", + // [9-9] -> [8-9] + "[z-z][Z-Z][8-9]" + ])] + [CharacterClassModifyRange(@"[\u0600-\u06ff][\x34-\x37][\123-\347]", [ + @"[\u05ff-\u06ff][\x34-\x37][\123-\347]", + @"[\u0601-\u06ff][\x34-\x37][\123-\347]", + @"[\u0600-\u06fe][\x34-\x37][\123-\347]", + @"[\u0600-\u0700][\x34-\x37][\123-\347]", + + @"[\u0600-\u06ff][\x33-\x37][\123-\347]", + @"[\u0600-\u06ff][\x35-\x37][\123-\347]", + @"[\u0600-\u06ff][\x34-\x36][\123-\347]", + @"[\u0600-\u06ff][\x34-\x38][\123-\347]", + + @"[\u0600-\u06ff][\x34-\x37][\122-\347]", + @"[\u0600-\u06ff][\x34-\x37][\124-\347]", + @"[\u0600-\u06ff][\x34-\x37][\123-\346]", + @"[\u0600-\u06ff][\x34-\x37][\123-\350]" + ])] + [CharacterClassModifyRange(@"[a-\u0600][\u0040-z]", [ + @"[b-\u0600][\u0040-z]", + @"[a-\u05ff][\u0040-z]", + @"[a-\u0601][\u0040-z]", + @"[a-\u0600][\u003f-z]", + @"[a-\u0600][\u0041-z]", + @"[a-\u0600][\u0040-y]" + ])] + [CharacterClassModifyRange("[!-#][#-a][!-z][#-A][!-Z][#-1][!-8]", [ + "[!-#][#-b][!-z][#-A][!-Z][#-1][!-8]", + "[!-#][#-a][!-y][#-A][!-Z][#-1][!-8]", + "[!-#][#-a][!-z][#-B][!-Z][#-1][!-8]", + "[!-#][#-a][!-z][#-A][!-Y][#-1][!-8]", + "[!-#][#-a][!-z][#-A][!-Z][#-0][!-8]", + "[!-#][#-a][!-z][#-A][!-Z][#-2][!-8]", + "[!-#][#-a][!-z][#-A][!-Z][#-1][!-7]", + "[!-#][#-a][!-z][#-A][!-Z][#-1][!-9]" + ])] + [CharacterClassModifyRange(@"[\u0031-\u0031][1-\u0031][\u0031-1]", [ + @"[\u0030-\u0031][1-\u0031][\u0031-1]", + @"[\u0031-\u0032][1-\u0031][\u0031-1]", + @"[\u0031-\u0031][0-\u0031][\u0031-1]", + @"[\u0031-\u0031][1-\u0032][\u0031-1]", + @"[\u0031-\u0031][1-\u0031][\u0030-1]", + @"[\u0031-\u0031][1-\u0031][\u0031-2]", + ])] + [CharacterClassModifyRange(@"[\u0031-\u0032][1-\u0032][\u0031-2][1-2]", [ + @"[\u0030-\u0032][1-\u0032][\u0031-2][1-2]", + @"[\u0032-\u0032][1-\u0032][\u0031-2][1-2]", + @"[\u0031-\u0031][1-\u0032][\u0031-2][1-2]", + @"[\u0031-\u0033][1-\u0032][\u0031-2][1-2]", + @"[\u0031-\u0032][0-\u0032][\u0031-2][1-2]", + @"[\u0031-\u0032][2-\u0032][\u0031-2][1-2]", + @"[\u0031-\u0032][1-\u0031][\u0031-2][1-2]", + @"[\u0031-\u0032][1-\u0033][\u0031-2][1-2]", + @"[\u0031-\u0032][1-\u0032][\u0030-2][1-2]", + @"[\u0031-\u0032][1-\u0032][\u0032-2][1-2]", + @"[\u0031-\u0032][1-\u0032][\u0031-1][1-2]", + @"[\u0031-\u0032][1-\u0032][\u0031-3][1-2]", + @"[\u0031-\u0032][1-\u0032][\u0031-2][0-2]", + @"[\u0031-\u0032][1-\u0032][\u0031-2][2-2]", + @"[\u0031-\u0032][1-\u0032][\u0031-2][1-1]", + @"[\u0031-\u0032][1-\u0032][\u0031-2][1-3]" + ])] + public void CharacterClassModifyRange(string input, string[] expected) + { + // Act + var result = TestHelpers.ParseAndMutate(input, new CharacterClassRangeMutator()); + + // Assert + result.Select(static a => a.ReplacementPattern).ToArray().ShouldBeEquivalentTo(expected); + } + + [TestMethod] + [DataRow(@"[\a-\f]")] + [DataRow(@"[\ca-\cc]")] + [DataRow(@"[\t-\n]")] + public void DoesNotModifyNonAlphaNumericRanges(string pattern) + { + // Act + var result = TestHelpers.ParseAndMutate(pattern, new CharacterClassRangeMutator()); + + // Assert + result.ShouldBeEmpty(); + } + + [TestMethod] + public void ShouldMutateCharacterClassRange() + { + // Arrange + var leftNode = new CharacterNode('A'); + var rightNode = new CharacterNode('Z'); + var rangeNode = new CharacterClassRangeNode(leftNode, rightNode); + var rootNode = new CharacterClassNode(new CharacterClassCharacterSetNode(rangeNode), false); + var target = new CharacterClassRangeMutator(); + + // Act + var result = target.ApplyMutations(rangeNode, rootNode); + + // Assert + var regexMutations = result as RegexMutation[] ?? result.ToArray(); + regexMutations.Length.ShouldBe(2); + regexMutations.ElementAt(0).OriginalNode.ShouldBe(leftNode); + regexMutations.ElementAt(0).ReplacementNode.ToString().ShouldBe("B"); + regexMutations.ElementAt(0).ReplacementPattern.ShouldBe("[B-Z]"); + regexMutations.ElementAt(0).DisplayName.ShouldBe("Regex character class range modification"); + regexMutations.ElementAt(0).Description.ShouldBe("""Replaced character "A" with "B" at offset 1."""); + + regexMutations.ElementAt(1).OriginalNode.ShouldBe(rightNode); + regexMutations.ElementAt(1).ReplacementNode.ToString().ShouldBe("Y"); + regexMutations.ElementAt(1).ReplacementPattern.ShouldBe("[A-Y]"); + regexMutations.ElementAt(1).DisplayName.ShouldBe("Regex character class range modification"); + regexMutations.ElementAt(1).Description.ShouldBe("""Replaced character "Z" with "Y" at offset 3."""); + } + + [TestMethod] + public void ShouldNotMutateInvalidCharacterClassRange() + { + // Arrange + var rangeNode = new CharacterClassRangeNode(new AnyCharacterNode(), new AnyCharacterNode()); + var childNodes = new List + { + rangeNode + }; + var rootNode = new ConcatenationNode(childNodes); + var target = new CharacterClassRangeMutator(); + + // Act + var result = target.ApplyMutations(rangeNode, rootNode); + + // Assert + result.ShouldBeEmpty(); + } +} + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +file class CharacterClassModifyRangeAttribute : DataRowAttribute +{ + /// + public CharacterClassModifyRangeAttribute(string pattern, string[] expected) : base(pattern, expected) => + DisplayName = $"Character Class Modify Range {pattern}"; +} diff --git a/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/CharacterClassShorthandAnyCharMutatorTests.cs b/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/CharacterClassShorthandAnyCharMutatorTests.cs new file mode 100644 index 000000000..cd92514fd --- /dev/null +++ b/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/CharacterClassShorthandAnyCharMutatorTests.cs @@ -0,0 +1,72 @@ +using System; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using Stryker.Regex.Parser.Nodes; +using Stryker.RegexMutators.Mutators; + +namespace Stryker.RegexMutators.UnitTest.Mutators; + +[TestClass] +public sealed class CharacterClassShorthandAnyCharMutatorTests +{ + [TestMethod] + [CharacterClassShorthandAnyChar(@"\w\W\d\D\s\S", [ + @"[\w\W]\W\d\D\s\S", + @"\w[\W\w]\d\D\s\S", + @"\w\W[\d\D]\D\s\S", + @"\w\W\d[\D\d]\s\S", + @"\w\W\d\D[\s\S]\S", + @"\w\W\d\D\s[\S\s]" + ])] + public void CharacterClassToAnyChar(string input, string[] expected) + { + // Act + var result = TestHelpers.ParseAndMutate(input, new CharacterClassShorthandAnyCharMutator()); + + // Assert + result.Select(static a => a.ReplacementPattern).ToArray().ShouldBeEquivalentTo(expected); + } + + [TestMethod] + [DataRow("(abc)")] + [DataRow("(?:def)")] + [DataRow("Alice")] + [DataRow(@"\n+\t{2,}")] + public void DoesNotMutateNonCharacterClasses(string pattern) + { + // Act + var result = TestHelpers.ParseAndMutate(pattern, new CharacterClassShorthandAnyCharMutator()); + + // Assert + result.ShouldBeEmpty(); + } + + [TestMethod] + public void ShouldMutateCharacterClassShorthand() + { + // Arrange + var shorthandNode = new CharacterClassShorthandNode('w'); + var rootNode = new ConcatenationNode(shorthandNode); + var target = new CharacterClassShorthandAnyCharMutator(); + + // Act + var result = target.ApplyMutations(shorthandNode, rootNode); + + // Assert + var regexMutation = result.ShouldHaveSingleItem(); + regexMutation.OriginalNode.ShouldBe(shorthandNode); + regexMutation.ReplacementNode.ToString().ShouldBe(@"[\w\W]"); + regexMutation.ReplacementPattern.ShouldBe(@"[\w\W]"); + regexMutation.DisplayName.ShouldBe("Regex predefined character class to character class with its negation change"); + regexMutation.Description.ShouldBe("""Character class shorthand "\w" was replaced with "[\w\W]" at offset 1."""); + } +} + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +file class CharacterClassShorthandAnyCharAttribute : DataRowAttribute +{ + /// + public CharacterClassShorthandAnyCharAttribute(string pattern, string[] expected) : base(pattern, expected) => + DisplayName = $"""Changes Predefined Character Class to "[\w\W]" {pattern}"""; +} diff --git a/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/CharacterClassShorthandNullificationMutatorTests.cs b/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/CharacterClassShorthandNullificationMutatorTests.cs new file mode 100644 index 000000000..c1066f80c --- /dev/null +++ b/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/CharacterClassShorthandNullificationMutatorTests.cs @@ -0,0 +1,72 @@ +using System; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using Stryker.Regex.Parser.Nodes; +using Stryker.RegexMutators.Mutators; + +namespace Stryker.RegexMutators.UnitTest.Mutators; + +[TestClass] +public sealed class CharacterClassShorthandNullificationMutatorTests +{ + [TestMethod] + [CharacterClassShorthandNullification(@"\w\W\d\D\s\S", [ + @"w\W\d\D\s\S", + @"\wW\d\D\s\S", + @"\w\Wd\D\s\S", + @"\w\W\dD\s\S", + @"\w\W\d\Ds\S", + @"\w\W\d\D\sS" + ])] + public void CharacterClassToAnyChar(string input, string[] expected) + { + // Act + var result = TestHelpers.ParseAndMutate(input, new CharacterClassShorthandNullificationMutator()); + + // Assert + result.Select(static a => a.ReplacementPattern).ToArray().ShouldBeEquivalentTo(expected); + } + + [TestMethod] + [DataRow("(abc)")] + [DataRow("(?:def)")] + [DataRow("Alice")] + [DataRow(@"\n+\t{2,}")] + public void DoesNotMutateNonCharacterClasses(string pattern) + { + // Act + var result = TestHelpers.ParseAndMutate(pattern, new CharacterClassShorthandNullificationMutator()); + + // Assert + result.ShouldBeEmpty(); + } + + [TestMethod] + public void ShouldMutateCharacterClassShorthand() + { + // Arrange + var shorthandNode = new CharacterClassShorthandNode('w'); + var rootNode = new ConcatenationNode(shorthandNode); + var target = new CharacterClassShorthandNullificationMutator(); + + // Act + var result = target.ApplyMutations(shorthandNode, rootNode); + + // Assert + var regexMutation = result.ShouldHaveSingleItem(); + regexMutation.OriginalNode.ShouldBe(shorthandNode); + regexMutation.ReplacementNode.ToString().ShouldBe("w"); + regexMutation.ReplacementPattern.ShouldBe("w"); + regexMutation.DisplayName.ShouldBe("Regex predefined character class nullification"); + regexMutation.Description.ShouldBe("""Character class shorthand "\w" was replaced with "w" at offset 0."""); + } +} + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +file class CharacterClassShorthandNullificationAttribute : DataRowAttribute +{ + /// + public CharacterClassShorthandNullificationAttribute(string pattern, string[] expected) : base(pattern, expected) => + DisplayName = $"Nullifies Predefined Character Class {pattern}"; +} diff --git a/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/CharacterClassToAnyCharMutatorTests.cs b/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/CharacterClassToAnyCharMutatorTests.cs new file mode 100644 index 000000000..af0b52fad --- /dev/null +++ b/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/CharacterClassToAnyCharMutatorTests.cs @@ -0,0 +1,82 @@ +using System; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using Stryker.Regex.Parser.Nodes; +using Stryker.Regex.Parser.Nodes.CharacterClass; +using Stryker.RegexMutators.Mutators; + +namespace Stryker.RegexMutators.UnitTest.Mutators; + +[TestClass] +public sealed class CharacterClassToAnyCharMutatorTests +{ + [TestMethod] + [CharacterClassToAnyChar("[abc]", [@"[\w\W]"])] + [CharacterClassToAnyChar("[abc][bcd]", [@"[\w\W][bcd]", @"[abc][\w\W]"])] + [CharacterClassToAnyChar(@"[\w\W][bcd]", [@"[\w\W][\w\W]"])] + public void CharacterClassToAnyChar(string input, string[] expected) + { + // Act + var result = TestHelpers.ParseAndMutate(input, new CharacterClassToAnyCharMutator()); + + // Assert + result.Select(static a => a.ReplacementPattern).ToArray().ShouldBeEquivalentTo(expected); + } + + [TestMethod] + [DataRow(@"[\w\W]")] + [DataRow(@"[\W\w]")] + public void DoesNotMutateToItself(string pattern) + { + // Act + var result = TestHelpers.ParseAndMutate(pattern, new CharacterClassToAnyCharMutator()); + + // Assert + result.ShouldBeEmpty(); + } + + [TestMethod] + [DataRow("(abc)")] + [DataRow("(?:def)")] + [DataRow("Alice")] + [DataRow(@"\d+\w{2,}")] + public void DoesNotMutateNonCharacterClasses(string pattern) + { + // Act + var result = TestHelpers.ParseAndMutate(pattern, new CharacterClassToAnyCharMutator()); + + // Assert + result.ShouldBeEmpty(); + } + + [TestMethod] + public void ShouldMutateCharacterClass() + { + // Arrange + var classNode = new CharacterClassNode(new CharacterClassCharacterSetNode([ + new CharacterNode('a'), new CharacterNode('b'), new CharacterNode('c') + ]), false); + var rootNode = new ConcatenationNode(classNode); + var target = new CharacterClassToAnyCharMutator(); + + // Act + var result = target.ApplyMutations(classNode, rootNode); + + // Assert + var regexMutation = result.ShouldHaveSingleItem(); + regexMutation.OriginalNode.ShouldBe(classNode); + regexMutation.ReplacementNode.ToString().ShouldBe(@"[\w\W]"); + regexMutation.ReplacementPattern.ShouldBe(@"[\w\W]"); + regexMutation.DisplayName.ShouldBe("""Regex character class to "[\w\W]" change"""); + regexMutation.Description.ShouldBe("""Replaced regex node "[abc]" with "[\w\W]" at offset 0."""); + } +} + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +file class CharacterClassToAnyCharAttribute : DataRowAttribute +{ + /// + public CharacterClassToAnyCharAttribute(string pattern, string[] expected) : base(pattern, expected) => + DisplayName = $"Character Class to any char {pattern}"; +} diff --git a/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/GroupToNcGroupMutatorTests.cs b/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/GroupToNcGroupMutatorTests.cs new file mode 100644 index 000000000..7e0a23ba3 --- /dev/null +++ b/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/GroupToNcGroupMutatorTests.cs @@ -0,0 +1,70 @@ +using System; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using Stryker.Regex.Parser.Nodes; +using Stryker.Regex.Parser.Nodes.GroupNodes; +using Stryker.RegexMutators.Mutators; + +namespace Stryker.RegexMutators.UnitTest.Mutators; + +[TestClass] +public sealed class GroupToNcGroupMutatorTests +{ + [TestMethod] + [GroupToNcGroup("([abc])", ["(?:[abc])"])] + [GroupToNcGroup("([abc][bcd])", ["(?:[abc][bcd])"])] + [GroupToNcGroup(@"([\w\W])([bcd])", [@"(?:[\w\W])([bcd])", @"([\w\W])(?:[bcd])"])] + [GroupToNcGroup("(([bcd])([bcd]))", ["((?:[bcd])([bcd]))", "(([bcd])(?:[bcd]))", "(?:([bcd])([bcd]))"])] + public void GroupToNcGroup(string input, string[] expected) + { + // Act + var result = TestHelpers.ParseAndMutate(input, new GroupToNcGroupMutator()); + + // Assert + result.Select(static a => a.ReplacementPattern).ToArray().ShouldBeEquivalentTo(expected); + } + + [TestMethod] + [DataRow("(?:def)")] + [DataRow("Alice")] + [DataRow(@"\d+\w{2,}")] + public void DoesNotMutateNonCaptureGroups(string pattern) + { + // Act + var result = TestHelpers.ParseAndMutate(pattern, new GroupToNcGroupMutator()); + + // Assert + result.ShouldBeEmpty(); + } + + [TestMethod] + public void FlipsCaptureGroupToNonCaptureGroup() + { + // Arrange + var lookaroundGroupNode = new CaptureGroupNode([ + new CharacterNode('f'), new CharacterNode('o'), new CharacterNode('o') + ]); + var rootNode = new ConcatenationNode(lookaroundGroupNode); + var target = new GroupToNcGroupMutator(); + + // Act + var result = target.ApplyMutations(lookaroundGroupNode, rootNode).ToList(); + + // Assert + var mutation = result.ShouldHaveSingleItem(); + mutation.OriginalNode.ShouldBe(lookaroundGroupNode); + mutation.ReplacementNode.ToString().ShouldBe("(?:foo)"); + mutation.ReplacementPattern.ShouldBe("(?:foo)"); + mutation.DisplayName.ShouldBe("Regex capturing group to non-capturing group modification"); + mutation.Description.ShouldBe("""Capturing group "(foo)" was replaced with "(?:foo)" at offset 0."""); + } +} + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +file class GroupToNcGroupAttribute : DataRowAttribute +{ + /// + public GroupToNcGroupAttribute(string pattern, string[] expected) : base(pattern, expected) => + DisplayName = $"Capture group to non capture group {pattern}"; +} diff --git a/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/QuantifierReluctantAdditionMutatorTests.cs b/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/QuantifierReluctantAdditionMutatorTests.cs new file mode 100644 index 000000000..57dc07684 --- /dev/null +++ b/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/QuantifierReluctantAdditionMutatorTests.cs @@ -0,0 +1,87 @@ +using System; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using Stryker.Regex.Parser.Nodes; +using Stryker.Regex.Parser.Nodes.CharacterClass; +using Stryker.Regex.Parser.Nodes.QuantifierNodes; +using Stryker.RegexMutators.Mutators; + +namespace Stryker.RegexMutators.UnitTest.Mutators; + +[TestClass] +public sealed class QuantifierReluctantAdditionMutatorTests +{ + [TestMethod] + [QuantifierReluctantAddition("ab+", ["ab+?"])] + [QuantifierReluctantAddition("ab*", ["ab*?"])] + [QuantifierReluctantAddition("ab?", ["ab??"])] + [QuantifierReluctantAddition("ab{2,}", ["ab{2,}?"])] + public void QuantifierReluctantAddition(string input, string[] expected) + { + // Act + var result = TestHelpers.ParseAndMutate(input, new QuantifierReluctantAdditionMutator()); + + // Assert + result.Select(static a => a.ReplacementPattern).ToArray().ShouldBeEquivalentTo(expected); + } + + [TestMethod] + [DataRow("ab+?")] + [DataRow("ab*?")] + [DataRow("ab??")] + [DataRow("ab{2,}?")] + public void DoesNotMutateLazyQuantifierNodes(string pattern) + { + // Act + var result = TestHelpers.ParseAndMutate(pattern, new QuantifierReluctantAdditionMutator()); + + // Assert + result.ShouldBeEmpty(); + } + + [TestMethod] + [DataRow("(abc)")] + [DataRow("(?:def)")] + [DataRow("Alice")] + [DataRow(@"\d\w")] + public void DoesNotMutateNonQuantityNodes(string pattern) + { + // Act + var result = TestHelpers.ParseAndMutate(pattern, new QuantifierReluctantAdditionMutator()); + + // Assert + result.ShouldBeEmpty(); + } + + [TestMethod] + public void ShouldMutateQuantifier() + { + // Arrange + var classNode = new CharacterClassNode(new CharacterClassCharacterSetNode([ + new CharacterNode('a'), new CharacterNode('b'), new CharacterNode('c') + ]), false); + var quantity = new QuantifierPlusNode(classNode); + var rootNode = new ConcatenationNode(quantity); + var target = new QuantifierReluctantAdditionMutator(); + + // Act + var result = target.ApplyMutations(quantity, rootNode); + + // Assert + var regexMutation = result.ShouldHaveSingleItem(); + regexMutation.OriginalNode.ShouldBe(quantity); + regexMutation.ReplacementNode.ToString().ShouldBe("[abc]+?"); + regexMutation.ReplacementPattern.ShouldBe("[abc]+?"); + regexMutation.DisplayName.ShouldBe("Regex greedy quantifier to reluctant quantifier modification"); + regexMutation.Description.ShouldBe("""Quantifier "[abc]+" was replace with "[abc]+?" at offset 5."""); + } +} + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +file class QuantifierReluctantAdditionAttribute : DataRowAttribute +{ + /// + public QuantifierReluctantAdditionAttribute(string pattern, string[] expected) : base(pattern, expected) => + DisplayName = $"Greedy quantifier to reluctant quantifier modification {pattern}"; +} diff --git a/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/QuantifierShortMutatorTests.cs b/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/QuantifierShortMutatorTests.cs new file mode 100644 index 000000000..59b91ec89 --- /dev/null +++ b/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/QuantifierShortMutatorTests.cs @@ -0,0 +1,133 @@ +using System; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using Stryker.Regex.Parser.Nodes; +using Stryker.Regex.Parser.Nodes.CharacterClass; +using Stryker.Regex.Parser.Nodes.QuantifierNodes; +using Stryker.RegexMutators.Mutators; + +namespace Stryker.RegexMutators.UnitTest.Mutators; + +[TestClass] +public sealed class QuantifierShortMutatorTests +{ + [TestMethod] + [QuantifierShort("abc?", [ + "abc{1,1}", "abc{0,0}", "abc{0,2}" + ])] + [QuantifierShort("abc*", [ + "abc{1,}" + ])] + [QuantifierShort("abc+", [ + "abc{0,}", "abc{2,}" + ])] + [QuantifierShort("a?b*c+", [ + "a{1,1}b*c+", "a{0,0}b*c+", "a{0,2}b*c+", "a?b{1,}c+", "a?b*c{0,}", "a?b*c{2,}" + ])] + public void QuantifierShort(string input, string[] expected) + { + // Act + var result = TestHelpers.ParseAndMutate(input, new QuantifierShortMutator()); + + // Assert + result.Select(static a => a.ReplacementPattern).ToArray().ShouldBeEquivalentTo(expected); + } + + [TestMethod] + [QuantifierShort("c+", [ + "c{0,}", "c{2,}" + ])] + public void QuantifierShort2(string input, string[] expected) + { + // Act + var result = TestHelpers.ParseAndMutate(input, new QuantifierShortMutator()); + + // Assert + result.Select(static a => a.ReplacementPattern).ToArray().ShouldBeEquivalentTo(expected); + } + + [TestMethod] + public void ShouldMutatePlusQuantifier() + { + // Arrange + var classNode = new CharacterClassNode(new CharacterClassCharacterSetNode([ + new CharacterNode('a'), new CharacterNode('b'), new CharacterNode('c') + ]), false); + var quantity = new QuantifierPlusNode(classNode); + var rootNode = new ConcatenationNode(quantity); + var target = new QuantifierShortMutator(); + + // Act + var result = target.ApplyMutations(quantity, rootNode); + + // Assert + var regexMutations = result as RegexMutation[] ?? result.ToArray(); + regexMutations.Length.ShouldBe(2); + regexMutations.ElementAt(0).OriginalNode.ShouldBe(quantity); + regexMutations.ElementAt(0).ReplacementNode.ToString().ShouldBe("[abc]{0,}"); + regexMutations.ElementAt(0).ReplacementPattern.ShouldBe("[abc]{0,}"); + regexMutations.ElementAt(0).DisplayName.ShouldBe("Regex greedy quantifier quantity mutation"); + + regexMutations.ElementAt(0) + .Description.ShouldBe("""Quantifier "[abc]+" was replaced with "[abc]{0,}" at offset 5."""); + + regexMutations.ElementAt(1).OriginalNode.ShouldBe(quantity); + regexMutations.ElementAt(1).ReplacementNode.ToString().ShouldBe("[abc]{2,}"); + regexMutations.ElementAt(1).ReplacementPattern.ShouldBe("[abc]{2,}"); + regexMutations.ElementAt(1).DisplayName.ShouldBe("Regex greedy quantifier quantity mutation"); + + regexMutations.ElementAt(1) + .Description.ShouldBe("""Quantifier "[abc]+" was replaced with "[abc]{2,}" at offset 5."""); + } + + [TestMethod] + public void ShouldMutateQuestionMarkQuantifier() + { + // Arrange + var classNode = new CharacterClassNode(new CharacterClassCharacterSetNode([ + new CharacterNode('a'), new CharacterNode('b'), new CharacterNode('c') + ]), false); + var quantity = new QuantifierQuestionMarkNode(classNode); + var rootNode = new ConcatenationNode(quantity); + var target = new QuantifierShortMutator(); + + // Act + var result = target.ApplyMutations(quantity, rootNode); + + // Assert + var regexMutations = result as RegexMutation[] ?? result.ToArray(); + regexMutations.Length.ShouldBe(3); + regexMutations.ElementAt(0).OriginalNode.ShouldBe(quantity); + regexMutations.ElementAt(0).ReplacementNode.ToString().ShouldBe("[abc]{1,1}"); + regexMutations.ElementAt(0).ReplacementPattern.ShouldBe("[abc]{1,1}"); + regexMutations.ElementAt(0).DisplayName.ShouldBe("Regex greedy quantifier quantity mutation"); + + regexMutations.ElementAt(0) + .Description.ShouldBe("""Quantifier "[abc]?" was replaced with "[abc]{1,1}" at offset 5."""); + + regexMutations.ElementAt(1).OriginalNode.ShouldBe(quantity); + regexMutations.ElementAt(1).ReplacementNode.ToString().ShouldBe("[abc]{0,0}"); + regexMutations.ElementAt(1).ReplacementPattern.ShouldBe("[abc]{0,0}"); + regexMutations.ElementAt(1).DisplayName.ShouldBe("Regex greedy quantifier quantity mutation"); + + regexMutations.ElementAt(1) + .Description.ShouldBe("""Quantifier "[abc]?" was replaced with "[abc]{0,0}" at offset 5."""); + + regexMutations.ElementAt(2).OriginalNode.ShouldBe(quantity); + regexMutations.ElementAt(2).ReplacementNode.ToString().ShouldBe("[abc]{0,2}"); + regexMutations.ElementAt(2).ReplacementPattern.ShouldBe("[abc]{0,2}"); + regexMutations.ElementAt(2).DisplayName.ShouldBe("Regex greedy quantifier quantity mutation"); + + regexMutations.ElementAt(2) + .Description.ShouldBe("""Quantifier "[abc]?" was replaced with "[abc]{0,2}" at offset 5."""); + } +} + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +file class QuantifierShortAttribute : DataRowAttribute +{ + /// + public QuantifierShortAttribute(string pattern, string[] expected) : base(pattern, expected) => + DisplayName = $"Modifies short quantifier {pattern}"; +} diff --git a/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/TestHelpers.cs b/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/TestHelpers.cs new file mode 100644 index 000000000..453408f44 --- /dev/null +++ b/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/TestHelpers.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Stryker.Regex.Parser; +using Stryker.Regex.Parser.Nodes; +using Stryker.RegexMutators.Mutators; +using System.Linq; + +namespace Stryker.RegexMutators.UnitTest.Mutators; + +public static class TestHelpers +{ + public static IEnumerable ParseAndMutate(string pattern, RegexMutatorBase mutator) + where T : RegexNode + { + var root = new Parser(pattern).Parse().Root; + IEnumerable allNodes = [..root.GetDescendantNodes(), root]; + return allNodes.Where(((IRegexMutator)mutator).CanHandle).OfType().SelectMany(node => mutator.ApplyMutations(node, root)); + } +} diff --git a/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/UnicodeCharClassNegationMutatorTests.cs b/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/UnicodeCharClassNegationMutatorTests.cs new file mode 100644 index 000000000..d1bfe6ead --- /dev/null +++ b/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/Mutators/UnicodeCharClassNegationMutatorTests.cs @@ -0,0 +1,142 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using Stryker.Regex.Parser.Nodes; +using Stryker.RegexMutators.Mutators; + +namespace Stryker.RegexMutators.UnitTest.Mutators; + +[TestClass] +public sealed class UnicodeCharClassNegationMutatorTests +{ + [TestMethod] + public void NegatesUnicodeCharacterClassWithLoneProperty() + { + // Act + var result = + TestHelpers.ParseAndMutate(@"\p{IsBasicLatin}\P{IsBasicLatin}", new UnicodeCharClassNegationMutator()); + + // Assert + result.Select(static a => a.ReplacementPattern) + .ToArray() + .ShouldBeEquivalentTo((string[]) + [ + @"\P{IsBasicLatin}\P{IsBasicLatin}", @"\p{IsBasicLatin}\p{IsBasicLatin}" + ]); + } + + [TestMethod] + [Ignore("Dotnet does not support this regex expression")] + public void NegatesUnicodeCharacterClassWithPropertyAndValue() + { + // Act + var result = TestHelpers.ParseAndMutate(@"\p{Script_Extensions=Latin}\P{Script_Extensions=Latin}", + new UnicodeCharClassNegationMutator()); + + // Assert + result.Select(static a => a.ReplacementPattern) + .ToArray() + .ShouldBeEquivalentTo((string[]) + [ + @"\P{Script_Extensions=Latin}\P{Script_Extensions=Latin}", + @"\p{Script_Extensions=Latin}\p{Script_Extensions=Latin}" + ]); + } + + [TestMethod] + public void ShouldNegateUnicodeCharacterClassAtStart() + { + // Arrange + var unicodeNode = new UnicodeCategoryNode("IsBasicLatin", false); + + var childNodes = new List + { + unicodeNode, new CharacterNode('a'), new CharacterNode('b'), new CharacterNode('c') + }; + var rootNode = new ConcatenationNode(childNodes); + var target = new UnicodeCharClassNegationMutator(); + + // Act + var result = target.ApplyMutations(unicodeNode, rootNode); + + // Assert + var mutation = result.ShouldHaveSingleItem(); + mutation.OriginalNode.ShouldBe(unicodeNode); + mutation.ReplacementNode.ToString().ShouldBe(@"\P{IsBasicLatin}"); + mutation.ReplacementPattern.ShouldBe(@"\P{IsBasicLatin}abc"); + mutation.DisplayName.ShouldBe("Regex Unicode character class negation mutation"); + + mutation.Description + .ShouldBe("""Unicode category "\p{IsBasicLatin}" was replaced with "\P{IsBasicLatin}" at offset 0."""); + } + + [TestMethod] + public void ShouldNegateUnicodeCharacterClassInMiddle() + { + // Arrange + var unicodeNode = new UnicodeCategoryNode("IsBasicLatin", false); + + var childNodes = new List + { + new CharacterNode('a'), unicodeNode, new CharacterNode('b'), new CharacterNode('c') + }; + var rootNode = new ConcatenationNode(childNodes); + var target = new UnicodeCharClassNegationMutator(); + + // Act + var result = target.ApplyMutations(unicodeNode, rootNode); + + // Assert + var mutation = result.ShouldHaveSingleItem(); + mutation.OriginalNode.ShouldBe(unicodeNode); + mutation.ReplacementNode.ToString().ShouldBe(@"\P{IsBasicLatin}"); + mutation.ReplacementPattern.ShouldBe(@"a\P{IsBasicLatin}bc"); + mutation.DisplayName.ShouldBe("Regex Unicode character class negation mutation"); + + mutation.Description + .ShouldBe("""Unicode category "\p{IsBasicLatin}" was replaced with "\P{IsBasicLatin}" at offset 1."""); + } + + [TestMethod] + public void ShouldNegateUnicodeCharacterClassAtEnd() + { + // Arrange + var unicodeNode = new UnicodeCategoryNode("IsBasicLatin", false); + + var childNodes = new List + { + new CharacterNode('a'), new CharacterNode('b'), new CharacterNode('c'), unicodeNode + }; + var rootNode = new ConcatenationNode(childNodes); + var target = new UnicodeCharClassNegationMutator(); + + // Act + var result = target.ApplyMutations(unicodeNode, rootNode); + + // Assert + var mutation = result.ShouldHaveSingleItem(); + mutation.OriginalNode.ShouldBe(unicodeNode); + mutation.ReplacementNode.ToString().ShouldBe(@"\P{IsBasicLatin}"); + mutation.ReplacementPattern.ShouldBe(@"abc\P{IsBasicLatin}"); + mutation.DisplayName.ShouldBe("Regex Unicode character class negation mutation"); + + mutation.Description + .ShouldBe("""Unicode category "\p{IsBasicLatin}" was replaced with "\P{IsBasicLatin}" at offset 3."""); + } + + [TestMethod] + public void MutateShouldNotMutateNonUnicodeCharacterNode() + { + // Arrange + var characterNode = new CharacterNode('a'); + var rootNode = new ConcatenationNode(characterNode); + var target = new UnicodeCharClassNegationMutator(); + + // Act + var result = target.Mutate(characterNode, rootNode); + + // Assert + result.ShouldBeEmpty(); + } +} diff --git a/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/RegexMutantOrchestratorTest.cs b/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/RegexMutantOrchestratorTest.cs index 1c46865a2..7ae230d35 100644 --- a/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/RegexMutantOrchestratorTest.cs +++ b/src/Stryker.RegexMutators/Stryker.RegexMutators.UnitTest/RegexMutantOrchestratorTest.cs @@ -35,7 +35,8 @@ public void ShouldRemoveQuantifier() var result = target.Mutate(); // Assert - var mutation = result.ShouldHaveSingleItem(); + result.Count().ShouldBe(5); + var mutation = result.ElementAt(0); mutation.OriginalNode.ToString().ShouldBe("X?"); mutation.ReplacementNode.ToString().ShouldBe("X"); mutation.ReplacementPattern.ShouldBe("abcX"); @@ -53,7 +54,8 @@ public void ShouldRemoveLazyQuantifier() var result = target.Mutate(); // Assert - var mutation = result.ShouldHaveSingleItem(); + result.Count().ShouldBe(4); + var mutation = result.ElementAt(0); mutation.OriginalNode.ToString().ShouldBe("X??"); mutation.ReplacementNode.ToString().ShouldBe("X"); mutation.ReplacementPattern.ShouldBe("abcX"); @@ -71,7 +73,8 @@ public void ShouldNegateUnnegatedCharacterClass() var result = target.Mutate(); // Assert - var mutation = result.ShouldHaveSingleItem(); + result.Count().ShouldBe(4); + var mutation = result.ElementAt(0); mutation.OriginalNode.ToString().ShouldBe("[XY]"); mutation.ReplacementNode.ToString().ShouldBe("[^XY]"); mutation.ReplacementPattern.ShouldBe("abc[^XY]"); @@ -89,7 +92,8 @@ public void ShouldUnnegateNegatedCharacterClass() var result = target.Mutate(); // Assert - var mutation = result.ShouldHaveSingleItem(); + result.Count().ShouldBe(4); + var mutation = result.ElementAt(0); mutation.OriginalNode.ToString().ShouldBe("[^XY]"); mutation.ReplacementNode.ToString().ShouldBe("[XY]"); mutation.ReplacementPattern.ShouldBe("abc[XY]"); @@ -107,7 +111,8 @@ public void ShouldNegateUnnegatedCharacterClassShorthand() var result = target.Mutate(); // Assert - var mutation = result.ShouldHaveSingleItem(); + result.Count().ShouldBe(3); + var mutation = result.ElementAt(0); mutation.OriginalNode.ToString().ShouldBe("\\d"); mutation.ReplacementNode.ToString().ShouldBe("\\D"); mutation.ReplacementPattern.ShouldBe("abc\\D"); @@ -125,7 +130,8 @@ public void ShouldUnnegateNegatedCharacterClassShorthand() var result = target.Mutate(); // Assert - var mutation = result.ShouldHaveSingleItem(); + result.Count().ShouldBe(3); + var mutation = result.ElementAt(0); mutation.OriginalNode.ToString().ShouldBe("\\D"); mutation.ReplacementNode.ToString().ShouldBe("\\d"); mutation.ReplacementPattern.ShouldBe("abc\\d"); @@ -145,4 +151,35 @@ public void ShouldApplyMultipleMutations() // Assert result.Count().ShouldBeGreaterThanOrEqualTo(4); } + + [TestMethod] + public void ShouldApplyMultipleMutations2() + { + // Arrange + var target = new RegexMutantOrchestrator("^abc(d+|[xyz])$"); + + // Act + var result = target.Mutate(); + + // Assert + result.Count().ShouldBeGreaterThanOrEqualTo(12); + } + + [TestMethod] + [DataRow(null)] + [DataRow("[Z-A]")] + [DataRow("\\")] + [DataRow("(abc")] + [DataRow(@"\p{UnicodeCategory}")] + public void InvalidRegexShouldNotThrow(string pattern) + { + // Arrange + var target = new RegexMutantOrchestrator(pattern); + + // Act + var result = target.Mutate(); + + // Assert + result.ShouldBeEmpty(); + } } diff --git a/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/CharacterClassChildRemovalMutator.cs b/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/CharacterClassChildRemovalMutator.cs new file mode 100644 index 000000000..78918843c --- /dev/null +++ b/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/CharacterClassChildRemovalMutator.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Linq; +using Stryker.Regex.Parser.Nodes; +using Stryker.Regex.Parser.Nodes.CharacterClass; + +namespace Stryker.RegexMutators.Mutators; + +public sealed class CharacterClassChildRemovalMutator : RegexMutatorBase, IRegexMutator +{ + /// + public override IEnumerable ApplyMutations(CharacterClassNode node, RegexNode root) + { + if (node.Subtraction is not null) + { + yield return new RegexMutation + { + OriginalNode = node.Subtraction, + ReplacementNode = null, + DisplayName = "Regex character class subtraction removal", + Description = + $"""Character Class Subtraction "-{node.Subtraction}" was removed at offset {node.Subtraction.GetSpan().Start - 1}.""", + ReplacementPattern = root.RemoveNode(node.Subtraction).ToString() + }; + + if (node.CharacterSet.ChildNodes.Count() == 1) + { + var replacementNode2 = new CharacterClassNode(node.Subtraction.CharacterSet, node.Negated); + + yield return new RegexMutation + { + OriginalNode = node, + ReplacementNode = replacementNode2, + DisplayName = "Regex character class subtraction replacement", + Description = + $"""Character Class "{node}" was replace with its subtraction "{replacementNode2}" at offset {node.GetSpan().Start}.""", + ReplacementPattern = root.ReplaceNode(node, replacementNode2).ToString() + }; + } + } + + if (node.CharacterSet.ChildNodes.Count() == 1) + { + yield break; + } + + foreach (var childNode in node.CharacterSet.ChildNodes) + { + yield return new RegexMutation + { + OriginalNode = childNode, + ReplacementNode = null, + DisplayName = "Regex character class child removal", + Description = + $"""Removed child "{childNode}" from character class "{node}" at offset {node.GetSpan().Start}.""", + ReplacementPattern = root.RemoveNode(childNode).ToString() + }; + } + } +} diff --git a/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/CharacterClassRangeMutator.cs b/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/CharacterClassRangeMutator.cs new file mode 100644 index 000000000..7ce9e0296 --- /dev/null +++ b/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/CharacterClassRangeMutator.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using Stryker.Regex.Parser.Nodes; +using Stryker.Regex.Parser.Nodes.CharacterClass; + +namespace Stryker.RegexMutators.Mutators; + +public sealed class CharacterClassRangeMutator : RegexMutatorBase, IRegexMutator +{ + /// + public override IEnumerable ApplyMutations(CharacterClassRangeNode node, RegexNode root) + { + foreach (var regexMutation in MutateRangePart(root, node.Start, node.End, ValidateStartNode)) + { + yield return regexMutation; + } + + foreach (var regexMutation in MutateRangePart(root, node.End, node.Start, ValidateEndNode)) + { + yield return regexMutation; + } + } + + private static RegexNode MutateNode(RegexNode n, int offset) => + n switch + { + CharacterNode cn => new CharacterNode((char)(cn.Character + offset)), + EscapeCharacterNode ecn when ecn.Escape[0] == 'x' => + EscapeCharacterNode.FromHex(Convert.ToString(ecn.Character + offset, 16).PadLeft(2, '0')), + EscapeCharacterNode ecn when ecn.Escape[0] == 'u' => + EscapeCharacterNode.FromUnicode(Convert.ToString(ecn.Character + offset, 16).PadLeft(4, '0')), + EscapeCharacterNode ecn when char.IsDigit(ecn.Escape[0]) => + EscapeCharacterNode.FromOctal(Convert.ToString(ecn.Character + offset, 8).PadLeft(3, '0')), + _ => null + }; + + private bool ValidateStartNode(RegexNode mutatedStartNode, RegexNode originalEndNode) => + (mutatedStartNode, originalEndNode) switch + { + (CharacterNode cs, CharacterNode ce) => cs.Character <= ce.Character && char.IsLetterOrDigit(cs.Character), + (CharacterNode cs, EscapeCharacterNode ece) => cs.Character <= ece.Character && + char.IsLetterOrDigit(cs.Character), + (EscapeCharacterNode ecs, CharacterNode ce) => ecs.Character <= ce.Character, + (EscapeCharacterNode ecs, EscapeCharacterNode ece) => ecs.Character <= ece.Character, + _ => false + }; + + private bool ValidateEndNode(RegexNode mutatedEndNode, RegexNode originalStartNode) => + (originalStartNode, mutatedEndNode) switch + { + (CharacterNode cs, CharacterNode ce) => cs.Character <= ce.Character && char.IsLetterOrDigit(ce.Character), + (CharacterNode cs, EscapeCharacterNode ece) => cs.Character <= ece.Character, + (EscapeCharacterNode ecs, CharacterNode ce) => ecs.Character <= ce.Character && + char.IsLetterOrDigit(ce.Character), + (EscapeCharacterNode ecs, EscapeCharacterNode ece) => ecs.Character <= ece.Character, + _ => false + }; + + private static IEnumerable MutateRangePart(RegexNode root, + RegexNode originalNode, + RegexNode otherNode, + Func validator) + { + var (start, _) = originalNode.GetSpan(); + + foreach (var replacementNode in new[] { MutateNode(originalNode, -1), MutateNode(originalNode, +1) }) + { + if (!validator(replacementNode, otherNode) || originalNode == replacementNode) + { + continue; + } + + yield return new RegexMutation + { + OriginalNode = originalNode, + ReplacementNode = replacementNode, + DisplayName = "Regex character class range modification", + Description = + $"""Replaced character "{originalNode}" with "{replacementNode}" at offset {start}.""", + ReplacementPattern = root.ReplaceNode(originalNode, replacementNode).ToString() + }; + } + } +} diff --git a/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/CharacterClassShorthandAnyCharMutator.cs b/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/CharacterClassShorthandAnyCharMutator.cs new file mode 100644 index 000000000..75169bd7b --- /dev/null +++ b/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/CharacterClassShorthandAnyCharMutator.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using Stryker.Regex.Parser.Nodes; +using Stryker.Regex.Parser.Nodes.CharacterClass; + +namespace Stryker.RegexMutators.Mutators; + +public sealed class CharacterClassShorthandAnyCharMutator : RegexMutatorBase, IRegexMutator +{ + /// + public override IEnumerable ApplyMutations(CharacterClassShorthandNode node, RegexNode root) + { + var replacementNode = new CharacterClassNode(new CharacterClassCharacterSetNode([ + node, + new CharacterClassShorthandNode(char.IsLower(node.Shorthand) + ? char.ToUpper(node.Shorthand) + : char.ToLower(node.Shorthand)) + ]), false); + + yield return new RegexMutation + { + OriginalNode = node, + ReplacementNode = replacementNode, + DisplayName = "Regex predefined character class to character class with its negation change", + Description = + $"""Character class shorthand "{node}" was replaced with "{replacementNode}" at offset {node.GetSpan().Start}.""", + ReplacementPattern = root.ReplaceNode(node, replacementNode).ToString() + }; + } +} diff --git a/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/CharacterClassShorthandNullificationMutator.cs b/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/CharacterClassShorthandNullificationMutator.cs new file mode 100644 index 000000000..ee974a357 --- /dev/null +++ b/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/CharacterClassShorthandNullificationMutator.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using Stryker.Regex.Parser.Nodes; + +namespace Stryker.RegexMutators.Mutators; + +public sealed class CharacterClassShorthandNullificationMutator : RegexMutatorBase, IRegexMutator +{ + /// + public override IEnumerable ApplyMutations(CharacterClassShorthandNode node, RegexNode root) + { + var replacementNode = new CharacterNode(node.Shorthand); + + yield return new RegexMutation + { + OriginalNode = node, + ReplacementNode = replacementNode, + DisplayName = "Regex predefined character class nullification", + Description = + $"""Character class shorthand "{node}" was replaced with "{replacementNode}" at offset {node.GetSpan().Start}.""", + ReplacementPattern = root.ReplaceNode(node, replacementNode).ToString() + }; + } +} diff --git a/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/CharacterClassToAnyCharMutator.cs b/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/CharacterClassToAnyCharMutator.cs new file mode 100644 index 000000000..3db34d544 --- /dev/null +++ b/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/CharacterClassToAnyCharMutator.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using Stryker.Regex.Parser.Nodes; +using Stryker.Regex.Parser.Nodes.CharacterClass; + +namespace Stryker.RegexMutators.Mutators; + +public sealed class CharacterClassToAnyCharMutator : RegexMutatorBase, IRegexMutator +{ + private static CharacterClassNode AnyChar => + new(new CharacterClassCharacterSetNode([new CharacterClassShorthandNode('w'), new CharacterClassShorthandNode('W')]), + false); + + /// + public override IEnumerable ApplyMutations(CharacterClassNode node, RegexNode root) + { + var replaceNode = root.ReplaceNode(node, AnyChar); + + if (node.CharacterSet.ChildNodes is List and + [ + CharacterClassShorthandNode { Shorthand: 'w' or 'W' }, + CharacterClassShorthandNode { Shorthand: 'w' or 'W' } + ] && node.Subtraction is null) + { + yield break; + } + + yield return new RegexMutation + { + OriginalNode = node, + ReplacementNode = AnyChar, + DisplayName = """Regex character class to "[\w\W]" change""", + Description = + $"""Replaced regex node "{node}" with "[\w\W]" at offset {node.GetSpan().Start}.""", + ReplacementPattern = replaceNode.ToString() + }; + } +} diff --git a/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/GroupToNcGroupMutator.cs b/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/GroupToNcGroupMutator.cs new file mode 100644 index 000000000..7f01adb71 --- /dev/null +++ b/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/GroupToNcGroupMutator.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using Stryker.Regex.Parser.Nodes; +using Stryker.Regex.Parser.Nodes.GroupNodes; + +namespace Stryker.RegexMutators.Mutators; + +public sealed class GroupToNcGroupMutator : RegexMutatorBase, IRegexMutator +{ + public override IEnumerable ApplyMutations(CaptureGroupNode node, RegexNode root) + { + var replacementNode = new NonCaptureGroupNode(node.ChildNodes); + + yield return new RegexMutation + { + OriginalNode = node, + ReplacementNode = replacementNode, + DisplayName = "Regex capturing group to non-capturing group modification", + Description = + $"""Capturing group "{node}" was replaced with "{replacementNode}" at offset {node.GetSpan().Start}.""", + ReplacementPattern = root.ReplaceNode(node, replacementNode).ToString() + }; + } +} diff --git a/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/IRegexMutator.cs b/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/IRegexMutator.cs index 043a2640d..6219dc957 100644 --- a/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/IRegexMutator.cs +++ b/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/IRegexMutator.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Stryker.Regex.Parser.Nodes; namespace Stryker.RegexMutators.Mutators; @@ -6,4 +6,6 @@ namespace Stryker.RegexMutators.Mutators; public interface IRegexMutator { IEnumerable Mutate(RegexNode node, RegexNode root); -} \ No newline at end of file + + bool CanHandle(RegexNode node); +} diff --git a/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/QuantifierReluctantAdditionMutator.cs b/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/QuantifierReluctantAdditionMutator.cs new file mode 100644 index 000000000..e6130d846 --- /dev/null +++ b/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/QuantifierReluctantAdditionMutator.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using Stryker.Regex.Parser.Nodes; +using Stryker.Regex.Parser.Nodes.QuantifierNodes; + +namespace Stryker.RegexMutators.Mutators; + +public sealed class QuantifierReluctantAdditionMutator : RegexMutatorBase, IRegexMutator +{ + /// + bool IRegexMutator.CanHandle(RegexNode node) => node is QuantifierNode && node.Parent is not LazyNode; + + /// + public override IEnumerable ApplyMutations(QuantifierNode node, RegexNode root) + { + var replacementNode = new LazyNode(node); + + yield return new RegexMutation + { + OriginalNode = node, + ReplacementNode = replacementNode, + DisplayName = "Regex greedy quantifier to reluctant quantifier modification", + Description = + $"""Quantifier "{node}" was replace with "{replacementNode}" at offset {node.GetSpan().Start}.""", + ReplacementPattern = root.ReplaceNode(node, replacementNode).ToString() + }; + } +} diff --git a/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/QuantifierShortMutator.cs b/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/QuantifierShortMutator.cs new file mode 100644 index 000000000..8f0d60ce5 --- /dev/null +++ b/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/QuantifierShortMutator.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Stryker.Regex.Parser.Nodes; +using Stryker.Regex.Parser.Nodes.QuantifierNodes; + +namespace Stryker.RegexMutators.Mutators; + +public sealed class QuantifierShortMutator : RegexMutatorBase, IRegexMutator +{ + private static readonly QuantifierQuantityMutator _innerNm = new(); + private static readonly QuantifierUnlimitedQuantityMutator _innerNOrMore = new(); + + /// + public override IEnumerable ApplyMutations(QuantifierNode node, RegexNode root) => + node switch + { + QuantifierNMNode or QuantifierNNode or QuantifierNOrMoreNode => [], + QuantifierStarNode => PlusOrStar(node, root, 0), + QuantifierPlusNode => PlusOrStar(node, root, 1), + QuantifierQuestionMarkNode => QuestionMark(node, root), + _ => throw new ArgumentOutOfRangeException(nameof(node)) + }; + + private static IEnumerable QuestionMark(QuantifierNode node, RegexNode root) + { + var newNode = node.Parent.ReplaceNode(node, new QuantifierNMNode(0, 1, node.ChildNodes.First()), false) + .ChildNodes.OfType().First(); + + return _innerNm.ApplyMutations(newNode, root) + .Select(a => new RegexMutation + { + OriginalNode = node, + ReplacementNode = a.ReplacementNode, + DisplayName = a.DisplayName, + Description = + a.Description.Replace($"\"{a.OriginalNode}\"", $"\"{node}\""), + ReplacementPattern = root.ReplaceNode(node, a.ReplacementNode).ToString() + }); + } + + private static IEnumerable PlusOrStar(QuantifierNode node, RegexNode root, int n) + { + var newNode = node.Parent.ReplaceNode(node, new QuantifierNOrMoreNode(n, node.ChildNodes.First()), false) + .ChildNodes.OfType().First(); + + return _innerNOrMore.ApplyMutations(newNode, root) + .Select(a => new RegexMutation + { + OriginalNode = node, + ReplacementNode = a.ReplacementNode, + DisplayName = a.DisplayName, + Description = + a.Description.Replace($"\"{a.OriginalNode}\"", $"\"{node}\""), + ReplacementPattern = + root.ReplaceNode(node, a.ReplacementNode).ToString() + }); + } +} diff --git a/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/RegexMutatorBase.cs b/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/RegexMutatorBase.cs index a79ab053b..4416e1e95 100644 --- a/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/RegexMutatorBase.cs +++ b/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/RegexMutatorBase.cs @@ -14,9 +14,9 @@ public abstract class RegexMutatorBase { public IEnumerable Mutate(RegexNode node, RegexNode root) { - if (node is T) + if (node is T regexNode) { - return ApplyMutations(node as T, root); + return ApplyMutations(regexNode, root); } return Enumerable.Empty(); @@ -28,4 +28,6 @@ public IEnumerable Mutate(RegexNode node, RegexNode root) /// The node to mutate /// One or more mutations public abstract IEnumerable ApplyMutations(T node, RegexNode root); + + public bool CanHandle(RegexNode node) => node is T; } diff --git a/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/UnicodeCharClassNegationMutator.cs b/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/UnicodeCharClassNegationMutator.cs new file mode 100644 index 000000000..7798e6e15 --- /dev/null +++ b/src/Stryker.RegexMutators/Stryker.RegexMutators/Mutators/UnicodeCharClassNegationMutator.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using Stryker.Regex.Parser.Nodes; + +namespace Stryker.RegexMutators.Mutators; + +public sealed class UnicodeCharClassNegationMutator : RegexMutatorBase, IRegexMutator +{ + /// + public override IEnumerable ApplyMutations(UnicodeCategoryNode node, RegexNode root) + { + var replacementNode = new UnicodeCategoryNode(node.Category, !node.Negated); + + yield return new RegexMutation + { + OriginalNode = node, + ReplacementNode = replacementNode, + DisplayName = "Regex Unicode character class negation mutation", + Description = + $"""Unicode category "{node}" was replaced with "{replacementNode}" at offset {node.GetSpan().Start}.""", + ReplacementPattern = root.ReplaceNode(node, replacementNode).ToString() + }; + } +} diff --git a/src/Stryker.RegexMutators/Stryker.RegexMutators/RegexMutantOrchestrator.cs b/src/Stryker.RegexMutators/Stryker.RegexMutators/RegexMutantOrchestrator.cs index 09a7d821d..c3756d144 100644 --- a/src/Stryker.RegexMutators/Stryker.RegexMutators/RegexMutantOrchestrator.cs +++ b/src/Stryker.RegexMutators/Stryker.RegexMutators/RegexMutantOrchestrator.cs @@ -1,64 +1,67 @@ -using System; using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; using Stryker.Regex.Parser; +using Stryker.Regex.Parser.Exceptions; using Stryker.Regex.Parser.Nodes; -using Stryker.Regex.Parser.Nodes.AnchorNodes; -using Stryker.Regex.Parser.Nodes.CharacterClass; -using Stryker.Regex.Parser.Nodes.GroupNodes; -using Stryker.Regex.Parser.Nodes.QuantifierNodes; using Stryker.RegexMutators.Mutators; namespace Stryker.RegexMutators; -public class RegexMutantOrchestrator +public sealed class RegexMutantOrchestrator(string pattern) { - private readonly string _pattern; - private readonly IDictionary> _mutatorsByRegexNodeType; - private RegexNode _root; + private static readonly IReadOnlyCollection _mutators = + [ + // Level 1 + new AnchorRemovalMutator(), + new CharacterClassNegationMutator(), + new CharacterClassShorthandNegationMutator(), + new QuantifierRemovalMutator(), + new UnicodeCharClassNegationMutator(), + new LookAroundMutator(), - public RegexMutantOrchestrator(string pattern) - { - _pattern = pattern; - _mutatorsByRegexNodeType = new Dictionary> - { - { typeof(AnchorNode), new List { new AnchorRemovalMutator() } }, - { typeof(QuantifierNode), new List { new QuantifierRemovalMutator() } }, - { typeof(CharacterClassNode), new List { new CharacterClassNegationMutator() } }, - { typeof(CharacterClassShorthandNode), new List { new CharacterClassShorthandNegationMutator() } }, - { typeof(QuantifierNOrMoreNode), new List { new QuantifierUnlimitedQuantityMutator() } }, - { typeof(QuantifierNMNode), new List { new QuantifierQuantityMutator() } }, - { typeof(LookaroundGroupNode), new List { new LookAroundMutator() } }, - }; - } + // Level 2 + new CharacterClassChildRemovalMutator(), + new CharacterClassToAnyCharMutator(), + new CharacterClassShorthandNullificationMutator(), + new CharacterClassShorthandAnyCharMutator(), + new QuantifierUnlimitedQuantityMutator(), + new QuantifierQuantityMutator(), + new QuantifierShortMutator(), + new GroupToNcGroupMutator(), + + // Level 3 + new CharacterClassRangeMutator(), + new QuantifierReluctantAdditionMutator() + ]; public IEnumerable Mutate() { + RegexNode root; + try { - var parser = new Parser(_pattern); + var parser = new Parser(pattern); var tree = parser.Parse(); - _root = tree.Root; + root = tree.Root; } - catch (RegexParseException) { yield break; } - var regexNodes = _root.GetDescendantNodes().ToList(); - regexNodes.Add(_root); + IEnumerable regexNodes = [..root.GetDescendantNodes(), root]; - foreach (var mutant in regexNodes.SelectMany(node => FindMutants(node, _root))) + foreach (var regexNode in regexNodes) { - yield return mutant; + foreach (var item in _mutators) + { + if (item.CanHandle(regexNode)) + { + foreach (var regexMutation in item.Mutate(regexNode, root)) + { + yield return regexMutation; + } + } + } } } - - private IEnumerable FindMutants(RegexNode regexNode, RegexNode root) => _mutatorsByRegexNodeType - .Where(item => regexNode.GetType() == item.Key || regexNode.GetType().IsSubclassOf(item.Key)) - .SelectMany(item => item.Value) - .SelectMany(mutator => mutator.Mutate(regexNode, root)); - -} \ No newline at end of file +} From d1a9ade0eb2fa9f15aef4d92e3549cb64372644e Mon Sep 17 00:00:00 2001 From: Adel Atzouza <102997068+Adel-Atzouza@users.noreply.github.com> Date: Sat, 19 Oct 2024 11:17:34 +0200 Subject: [PATCH 3/3] fix(reference alias): Properly support PackageReference Alias (#3070) * Fix Package Reference Alias and updated tests * Update src/Stryker.Core/Stryker.Core/Initialisation/Buildalyzer/IAnalyzerResultExtensions.cs --------- Co-authored-by: Amzir-A <102997068+Amzir-A@users.noreply.github.com> Co-authored-by: Rouke Broersma --- .../Compiling/CSharpCompilingProcessTests.cs | 27 ++++++++++++++----- .../Buildalyzer/IAnalyzerResultExtensions.cs | 21 +++++++++++---- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Compiling/CSharpCompilingProcessTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Compiling/CSharpCompilingProcessTests.cs index d12d02def..e8ddef0db 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Compiling/CSharpCompilingProcessTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Compiling/CSharpCompilingProcessTests.cs @@ -72,11 +72,11 @@ public int Subtract(int first, int second) } [TestMethod] - public void CompilingProcessTests_ShouldSupportReferenceAliases() + public void CompilingProcessTests_ShouldSupportPackageReferenceAliases() { var syntaxTree = CSharpSyntaxTree.ParseText(@" -extern alias Texte; -using System; +extern alias TestAlias; +using TestAlias::System; namespace ExampleProject { @@ -97,16 +97,31 @@ public int Subtract(int first, int second) properties: new Dictionary() { { "TargetDir", "" }, - { "AssemblyName", "AssemblyName"}, - { "TargetFileName", "TargetFileName.dll"}, + { "AssemblyName", "AssemblyName" }, + { "TargetFileName", "TargetFileName.dll" }, }, // add a reference to system so the example code can compile - references: new[] { typeof(object).Assembly.Location, $"Texte={typeof(StringBuilder).Assembly.Location}" } + references: new[] { typeof(object).Assembly.Location }, + packageReferences: new ReadOnlyDictionary>( + new Dictionary> + { + { + typeof(object).Assembly.GetName().Name, + new ReadOnlyDictionary( + new Dictionary + { + { "Aliases", "TestAlias" } + } + ) + } + } + ) ).Object } }; var rollbackProcessMock = new Mock(MockBehavior.Strict); + var target = new CsharpCompilingProcess(input, rollbackProcessMock.Object); using (var ms = new MemoryStream()) diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/Buildalyzer/IAnalyzerResultExtensions.cs b/src/Stryker.Core/Stryker.Core/Initialisation/Buildalyzer/IAnalyzerResultExtensions.cs index 60738a077..680e1817c 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/Buildalyzer/IAnalyzerResultExtensions.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/Buildalyzer/IAnalyzerResultExtensions.cs @@ -107,15 +107,26 @@ public static IEnumerable LoadReferences(this IAnalyzerResult { foreach (var reference in analyzerResult.References) { - if (reference.Contains('=')) + var referenceFileNameWithoutExtension = Path.GetFileNameWithoutExtension(reference); + string packageWithAlias = null; + + if (analyzerResult.PackageReferences is not null) + { + // Check for any matching package reference with an alias using LINQ + packageWithAlias = analyzerResult.PackageReferences + .Where(pr => pr.Key == referenceFileNameWithoutExtension && pr.Value.ContainsKey("Aliases")) + .Select(pr => pr.Value["Aliases"]) + .FirstOrDefault(); + } + + if (packageWithAlias is not null) { - // we have an alias - var split = reference.Split('='); - var aliases = split[0].Split(','); - yield return MetadataReference.CreateFromFile(split[1]).WithAliases(aliases); + // Return the reference with the alias + yield return MetadataReference.CreateFromFile(reference).WithAliases(new[] { packageWithAlias }); } else { + // If no alias is found, return the reference without aliases yield return MetadataReference.CreateFromFile(reference); } }