From b6632238389ff9bc9ec9f56ba33537c805aea01b Mon Sep 17 00:00:00 2001 From: Denis-RZ <77514212+Denis-RZ@users.noreply.github.com> Date: Sun, 8 Jun 2025 20:31:34 +0800 Subject: [PATCH] Add negative and integration tests --- .../CSharpModuleTests.cs | 368 ++++++++++++++++++ .../Modules/CSharpModule/CSharpModule.cs | 116 +++++- 2 files changed, 482 insertions(+), 2 deletions(-) diff --git a/UniversalCodePatcher.Tests/CSharpModuleTests.cs b/UniversalCodePatcher.Tests/CSharpModuleTests.cs index 01b8765..1c196a1 100644 --- a/UniversalCodePatcher.Tests/CSharpModuleTests.cs +++ b/UniversalCodePatcher.Tests/CSharpModuleTests.cs @@ -1,5 +1,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Linq; +using System.Text; using UniversalCodePatcher.Modules.CSharpModule; using UniversalCodePatcher.Models; @@ -17,6 +18,38 @@ public void Foo() { } } }"; + private const string PatternSample = @"namespace MyNamespace { + public class Service { + public void GetData() {} + public void SaveData() {} + } + }"; + + private const string AdvancedPatternSample = @"namespace MyNamespace { + public class Repository { + public void GetAdminUser() {} + public void SaveA() {} + } +}"; + + private const string SignatureSample = @"namespace Demo { + public class Data { + public int Calc(int x) { return x; } + } +}"; + + private static string GenerateClass(string name, params string[] members) + { + var sb = new StringBuilder(); + sb.AppendLine("namespace Generated {"); + sb.AppendLine($" public class {name} {{"); + foreach (var m in members) + sb.AppendLine($" {m}"); + sb.AppendLine(" }"); + sb.AppendLine("}"); + return sb.ToString(); + } + [TestMethod] public void AnalyzeCode_ExtractsElements() { @@ -48,5 +81,340 @@ public void ApplyPatch_AddPropertyToClass() var elements = module.AnalyzeCode(result.ModifiedCode, "CSharp").ToList(); Assert.IsTrue(elements.Any(e => e.Type == CodeElementType.Property && e.Name == "Added")); } + + [TestMethod] + public void SymbolMatches_WildcardAndRegex() + { + var module = new CSharpModule(); + module.Initialize(null); + var wildcardRule = new PatchRule + { + PatchType = PatchType.Delete, + TargetPattern = "*.Get*", + TargetLanguage = "CSharp", + TargetElementType = CodeElementType.Method + }; + Assert.IsTrue(module.CanApplyPatch(PatternSample, wildcardRule, "CSharp")); + + var regexRule = new PatchRule + { + PatchType = PatchType.Delete, + TargetPattern = "/Save.*Data/", + TargetLanguage = "CSharp", + TargetElementType = CodeElementType.Method + }; + Assert.IsTrue(module.CanApplyPatch(PatternSample, regexRule, "CSharp")); + } + + [TestMethod] + public void SymbolMatches_MorePatterns() + { + var module = new CSharpModule(); + module.Initialize(null); + + var wildcardRule = new PatchRule + { + PatchType = PatchType.Delete, + TargetPattern = "Save?", + TargetLanguage = "CSharp", + TargetElementType = CodeElementType.Method + }; + Assert.IsTrue(module.CanApplyPatch(AdvancedPatternSample, wildcardRule, "CSharp")); + + var regexRule = new PatchRule + { + PatchType = PatchType.Delete, + TargetPattern = "/Get.*User/", + TargetLanguage = "CSharp", + TargetElementType = CodeElementType.Method + }; + Assert.IsTrue(module.CanApplyPatch(AdvancedPatternSample, regexRule, "CSharp")); + } + + [TestMethod] + public void ApplyPatch_InsertBeforeAndAfter() + { + var module = new CSharpModule(); + module.Initialize(null); + + var beforeRule = new PatchRule + { + PatchType = PatchType.InsertBefore, + TargetPattern = "X", + TargetElementType = CodeElementType.Field, + TargetLanguage = "CSharp", + NewContent = "public int Y;" + }; + var afterRule = new PatchRule + { + PatchType = PatchType.InsertAfter, + TargetPattern = "Foo", + TargetElementType = CodeElementType.Method, + TargetLanguage = "CSharp", + NewContent = "public void Bar() {}" + }; + + var result1 = module.ApplyPatch(SampleCode, beforeRule, "CSharp"); + Assert.IsTrue(result1.Success, string.Join(";", result1.Errors)); + var result2 = module.ApplyPatch(result1.ModifiedCode, afterRule, "CSharp"); + Assert.IsTrue(result2.Success, string.Join(";", result2.Errors)); + Assert.IsTrue(result2.ModifiedCode.Contains("public int Y")); + Assert.IsTrue(result2.ModifiedCode.Contains("Bar()")); + } + + [TestMethod] + public void ApplyPatch_ChangeSignature() + { + var module = new CSharpModule(); + module.Initialize(null); + + var sigRule = new PatchRule + { + PatchType = PatchType.ChangeSignature, + TargetPattern = "Calc", + TargetElementType = CodeElementType.Method, + TargetLanguage = "CSharp", + NewContent = "public int Calc(int x, int y)" + }; + + var result = module.ApplyPatch(SignatureSample, sigRule, "CSharp"); + Assert.IsTrue(result.Success, string.Join(";", result.Errors)); + Assert.IsTrue(result.ModifiedCode.Contains("Calc(int x, int y)")); + } + + [TestMethod] + public void ApplyPatch_AddAttribute_ChangeVisibility() + { + var module = new CSharpModule(); + module.Initialize(null); + + var attrRule = new PatchRule + { + PatchType = PatchType.AddAttribute, + TargetPattern = "Derived", + TargetElementType = CodeElementType.Class, + TargetLanguage = "CSharp", + NewContent = "System.Obsolete" + }; + + var visRule = new PatchRule + { + PatchType = PatchType.ChangeVisibility, + TargetPattern = "X", + TargetElementType = CodeElementType.Field, + TargetLanguage = "CSharp", + NewContent = "private" + }; + + var r1 = module.ApplyPatch(SampleCode, attrRule, "CSharp"); + Assert.IsTrue(r1.Success, string.Join(";", r1.Errors)); + var r2 = module.ApplyPatch(r1.ModifiedCode, visRule, "CSharp"); + Assert.IsTrue(r2.Success, string.Join(";", r2.Errors)); + + Assert.IsTrue(r2.ModifiedCode.Contains("[System.Obsolete]")); + Assert.IsTrue(r2.ModifiedCode.Contains("private int X")); + } + + [TestMethod] + public void SymbolMatches_NegativePatterns() + { + var module = new CSharpModule(); + module.Initialize(null); + + var wildcardRule = new PatchRule + { + PatchType = PatchType.Delete, + TargetPattern = "*.Foo*", + TargetLanguage = "CSharp", + TargetElementType = CodeElementType.Method + }; + Assert.IsFalse(module.CanApplyPatch(PatternSample, wildcardRule, "CSharp")); + + var regexRule = new PatchRule + { + PatchType = PatchType.Delete, + TargetPattern = "/Non.*Exist/", + TargetLanguage = "CSharp", + TargetElementType = CodeElementType.Method + }; + Assert.IsFalse(module.CanApplyPatch(PatternSample, regexRule, "CSharp")); + } + + [TestMethod] + public void ApplyPatch_InsertBefore_NoMatch() + { + var code = GenerateClass("Neg", "public int A;", "public void Do() {}"); + var module = new CSharpModule(); + module.Initialize(null); + + var rule = new PatchRule + { + PatchType = PatchType.InsertBefore, + TargetPattern = "B", + TargetElementType = CodeElementType.Field, + TargetLanguage = "CSharp", + NewContent = "public int B;" + }; + + var result = module.ApplyPatch(code, rule, "CSharp"); + Assert.IsFalse(result.Success); + Assert.AreEqual(code, result.ModifiedCode); + Assert.IsTrue(result.Errors.Any()); + } + + [TestMethod] + public void ApplyPatch_InsertAfter_NoMatch() + { + var code = GenerateClass("Neg", "public int A;", "public void Do() {}"); + var module = new CSharpModule(); + module.Initialize(null); + + var rule = new PatchRule + { + PatchType = PatchType.InsertAfter, + TargetPattern = "Missing", + TargetElementType = CodeElementType.Method, + TargetLanguage = "CSharp", + NewContent = "public void Done() {}" + }; + + var result = module.ApplyPatch(code, rule, "CSharp"); + Assert.IsFalse(result.Success); + Assert.AreEqual(code, result.ModifiedCode); + Assert.IsTrue(result.Errors.Any()); + } + + [TestMethod] + public void ApplyPatch_ChangeSignature_NoMatch() + { + var code = GenerateClass("Neg", "public int A;", "public void Do(int x) {}"); + var module = new CSharpModule(); + module.Initialize(null); + + var rule = new PatchRule + { + PatchType = PatchType.ChangeSignature, + TargetPattern = "Unknown", + TargetElementType = CodeElementType.Method, + TargetLanguage = "CSharp", + NewContent = "public void Do(int x, int y)" + }; + + var result = module.ApplyPatch(code, rule, "CSharp"); + Assert.IsFalse(result.Success); + Assert.AreEqual(code, result.ModifiedCode); + Assert.IsTrue(result.Errors.Any()); + } + + [TestMethod] + public void ApplyPatch_AddAttribute_NoMatch() + { + var code = GenerateClass("Neg", "public int A;", "public void Do() {}"); + var module = new CSharpModule(); + module.Initialize(null); + + var rule = new PatchRule + { + PatchType = PatchType.AddAttribute, + TargetPattern = "MissingClass", + TargetElementType = CodeElementType.Class, + TargetLanguage = "CSharp", + NewContent = "System.Obsolete" + }; + + var result = module.ApplyPatch(code, rule, "CSharp"); + Assert.IsFalse(result.Success); + Assert.AreEqual(code, result.ModifiedCode); + Assert.IsTrue(result.Errors.Any()); + } + + [TestMethod] + public void ApplyPatch_ChangeVisibility_NoMatch() + { + var code = GenerateClass("Neg", "public int A;", "public void Do() {}"); + var module = new CSharpModule(); + module.Initialize(null); + + var rule = new PatchRule + { + PatchType = PatchType.ChangeVisibility, + TargetPattern = "B", + TargetElementType = CodeElementType.Field, + TargetLanguage = "CSharp", + NewContent = "private" + }; + + var result = module.ApplyPatch(code, rule, "CSharp"); + Assert.IsFalse(result.Success); + Assert.AreEqual(code, result.ModifiedCode); + Assert.IsTrue(result.Errors.Any()); + } + + [TestMethod] + public void Integration_MultipleSequentialPatches() + { + var code = GenerateClass("Integration", "public int X;", "public void Do(int v) {}"); + var module = new CSharpModule(); + module.Initialize(null); + + var beforeRule = new PatchRule + { + PatchType = PatchType.InsertBefore, + TargetPattern = "X", + TargetElementType = CodeElementType.Field, + TargetLanguage = "CSharp", + NewContent = "public int Y;" + }; + var afterRule = new PatchRule + { + PatchType = PatchType.InsertAfter, + TargetPattern = "Do", + TargetElementType = CodeElementType.Method, + TargetLanguage = "CSharp", + NewContent = "public void Done() {}" + }; + var sigRule = new PatchRule + { + PatchType = PatchType.ChangeSignature, + TargetPattern = "Do", + TargetElementType = CodeElementType.Method, + TargetLanguage = "CSharp", + NewContent = "public void Do(int v, string name)" + }; + var attrRule = new PatchRule + { + PatchType = PatchType.AddAttribute, + TargetPattern = "Integration", + TargetElementType = CodeElementType.Class, + TargetLanguage = "CSharp", + NewContent = "System.Obsolete" + }; + var visRule = new PatchRule + { + PatchType = PatchType.ChangeVisibility, + TargetPattern = "X", + TargetElementType = CodeElementType.Field, + TargetLanguage = "CSharp", + NewContent = "private" + }; + + var r1 = module.ApplyPatch(code, beforeRule, "CSharp"); + Assert.IsTrue(r1.Success, string.Join(";", r1.Errors)); + var r2 = module.ApplyPatch(r1.ModifiedCode, afterRule, "CSharp"); + Assert.IsTrue(r2.Success, string.Join(";", r2.Errors)); + var r3 = module.ApplyPatch(r2.ModifiedCode, sigRule, "CSharp"); + Assert.IsTrue(r3.Success, string.Join(";", r3.Errors)); + var r4 = module.ApplyPatch(r3.ModifiedCode, attrRule, "CSharp"); + Assert.IsTrue(r4.Success, string.Join(";", r4.Errors)); + var r5 = module.ApplyPatch(r4.ModifiedCode, visRule, "CSharp"); + Assert.IsTrue(r5.Success, string.Join(";", r5.Errors)); + + var finalCode = r5.ModifiedCode; + Assert.IsTrue(finalCode.Contains("[System.Obsolete]")); + Assert.IsTrue(finalCode.Contains("public int Y")); + Assert.IsTrue(finalCode.Contains("private int X")); + Assert.IsTrue(finalCode.Contains("void Done()")); + Assert.IsTrue(finalCode.Contains("Do(int v, string name)")); + } } } diff --git a/UniversalCodePatcher/Modules/CSharpModule/CSharpModule.cs b/UniversalCodePatcher/Modules/CSharpModule/CSharpModule.cs index c58feab..ff4402c 100644 --- a/UniversalCodePatcher/Modules/CSharpModule/CSharpModule.cs +++ b/UniversalCodePatcher/Modules/CSharpModule/CSharpModule.cs @@ -26,7 +26,14 @@ protected override bool OnInitialize() public IEnumerable SupportedPatchTypes => new[] { - PatchType.Replace, PatchType.InsertBefore, PatchType.InsertAfter, PatchType.Delete, PatchType.Modify + PatchType.Replace, + PatchType.InsertBefore, + PatchType.InsertAfter, + PatchType.Delete, + PatchType.Modify, + PatchType.ChangeSignature, + PatchType.AddAttribute, + PatchType.ChangeVisibility }; public IEnumerable SupportedLanguages => new[] { "CSharp" }; @@ -190,6 +197,26 @@ public PatchResult ApplyPatch(string originalCode, PatchRule rule, string langua case PatchType.Delete: newRoot = root.RemoveNode(targetNode, SyntaxRemoveOptions.KeepNoTrivia); break; + case PatchType.InsertBefore: + var beforeNode = SyntaxFactory.ParseMemberDeclaration(rule.NewContent); + if (beforeNode == null) + { + result.Errors.Add("Invalid new content"); + result.ModifiedCode = originalCode; + return result; + } + newRoot = root.InsertNodesBefore(targetNode, new[] { beforeNode }); + break; + case PatchType.InsertAfter: + var afterNode = SyntaxFactory.ParseMemberDeclaration(rule.NewContent); + if (afterNode == null) + { + result.Errors.Add("Invalid new content"); + result.ModifiedCode = originalCode; + return result; + } + newRoot = root.InsertNodesAfter(targetNode, new[] { afterNode }); + break; case PatchType.Modify when rule.TargetElementType == CodeElementType.Class: var classNode = targetNode as ClassDeclarationSyntax; var memberToAdd = SyntaxFactory.ParseMemberDeclaration(rule.NewContent); @@ -205,6 +232,72 @@ public PatchResult ApplyPatch(string originalCode, PatchRule rule, string langua return result; } break; + case PatchType.ChangeSignature: + if (targetNode is MethodDeclarationSyntax methodNode) + { + var sigNode = SyntaxFactory.ParseMemberDeclaration(rule.NewContent) as MethodDeclarationSyntax; + if (sigNode == null) + { + result.Errors.Add("Invalid new content"); + result.ModifiedCode = originalCode; + return result; + } + var updatedMethod = methodNode.WithParameterList(sigNode.ParameterList) + .WithReturnType(sigNode.ReturnType); + newRoot = root.ReplaceNode(methodNode, updatedMethod); + } + else + { + result.Errors.Add("ChangeSignature applicable only to methods"); + result.ModifiedCode = originalCode; + return result; + } + break; + case PatchType.AddAttribute: + if (targetNode is MemberDeclarationSyntax memberNode) + { + var text = rule.NewContent.Trim().Trim('[', ']'); + var attribute = SyntaxFactory.Attribute(SyntaxFactory.ParseName(text)); + var attrList = SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(attribute)); + var updatedMember = memberNode.AddAttributeLists(attrList); + newRoot = root.ReplaceNode(memberNode, updatedMember); + } + else + { + result.Errors.Add("AddAttribute target not supported"); + result.ModifiedCode = originalCode; + return result; + } + break; + case PatchType.ChangeVisibility: + if (targetNode is MemberDeclarationSyntax visibilityNode) + { + var accessibility = rule.NewContent.ToLower() switch + { + "public" => Accessibility.Public, + "private" => Accessibility.Private, + "protected" => Accessibility.Protected, + "internal" => Accessibility.Internal, + "protected internal" => Accessibility.ProtectedOrInternal, + "private protected" => Accessibility.ProtectedAndInternal, + _ => (Accessibility?)null + }; + if (accessibility == null) + { + result.Errors.Add("Invalid accessibility"); + result.ModifiedCode = originalCode; + return result; + } + var updatedVis = generator.WithAccessibility(visibilityNode, accessibility.Value); + newRoot = root.ReplaceNode(visibilityNode, updatedVis); + } + else + { + result.Errors.Add("ChangeVisibility target not supported"); + result.ModifiedCode = originalCode; + return result; + } + break; default: result.Errors.Add("PatchType not implemented"); result.ModifiedCode = originalCode; @@ -262,7 +355,26 @@ public PatchResult ApplyPatch(string originalCode, PatchRule rule, string langua private static bool SymbolMatches(ISymbol? symbol, string pattern) { if (symbol == null) return false; - return symbol.Name == pattern || symbol.ToDisplayString() == pattern; + + // Regex pattern between / / takes precedence + if (pattern.StartsWith("/") && pattern.EndsWith("/") && pattern.Length > 2) + { + var regex = new System.Text.RegularExpressions.Regex(pattern.Trim('/'), System.Text.RegularExpressions.RegexOptions.IgnoreCase); + return regex.IsMatch(symbol.Name) || regex.IsMatch(symbol.ToDisplayString()); + } + + // Wildcard support (* and ?) + if (pattern.Contains('*') || pattern.Contains('?')) + { + var regexPattern = "^" + System.Text.RegularExpressions.Regex.Escape(pattern) + .Replace("\\*", ".*") + .Replace("\\?", ".") + "$"; + var regex = new System.Text.RegularExpressions.Regex(regexPattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase); + return regex.IsMatch(symbol.Name) || regex.IsMatch(symbol.ToDisplayString()); + } + + return string.Equals(symbol.Name, pattern, StringComparison.OrdinalIgnoreCase) + || string.Equals(symbol.ToDisplayString(), pattern, StringComparison.OrdinalIgnoreCase); } public bool CanApplyPatch(string code, PatchRule rule, string language)