diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/NamingMethodPascalTests.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/NamingMethodPascalTests.cs index 8e540af5..3051d4d1 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/NamingMethodPascalTests.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer.Test/NamingMethodPascalTests.cs @@ -442,6 +442,99 @@ void IInterface.foo() { } VerifyCSharpDiagnostic(test, expected1, expected2); } + [TestMethod] + [Description("Test method with underscores should not trigger INTL0003")] + public void TestMethodWithUnderscores_MSTest_NoDiagnosticInformationReturned() + { + string test = @" + using System; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace ConsoleApplication1 + { + [TestClass] + public class TypeName + { + [TestMethod] + public void FooThing_IsFooThing_HasFooThing() + { + Assert.IsTrue(true); + } + } + }"; + + VerifyCSharpDiagnostic(test); + } + + [TestMethod] + [Description("Test method with underscores should not trigger INTL0003 - xUnit Fact")] + public void TestMethodWithUnderscores_XunitFact_NoDiagnosticInformationReturned() + { + string test = @" + using System; + using Xunit; + + namespace ConsoleApplication1 + { + public class TypeName + { + [Fact] + public void FooThing_IsFooThing_HasFooThing() + { + Assert.True(true); + } + } + }"; + + VerifyCSharpDiagnostic(test); + } + + [TestMethod] + [Description("Test method with underscores should not trigger INTL0003 - xUnit Theory")] + public void TestMethodWithUnderscores_XunitTheory_NoDiagnosticInformationReturned() + { + string test = @" + using System; + using Xunit; + + namespace ConsoleApplication1 + { + public class TypeName + { + [Theory] + public void FooThing_IsFooThing_HasFooThing() + { + Assert.True(true); + } + } + }"; + + VerifyCSharpDiagnostic(test); + } + + [TestMethod] + [Description("Test method with underscores should not trigger INTL0003 - NUnit Test")] + public void TestMethodWithUnderscores_NUnitTest_NoDiagnosticInformationReturned() + { + string test = @" + using System; + using NUnit.Framework; + + namespace ConsoleApplication1 + { + public class TypeName + { + [Test] + public void FooThing_IsFooThing_HasFooThing() + { + Assert.That(true, Is.True); + } + } + }"; + + VerifyCSharpDiagnostic(test); + } + protected override CodeFixProvider GetCSharpCodeFixProvider() { diff --git a/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/NamingMethodPascal.cs b/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/NamingMethodPascal.cs index b572f48c..6d976265 100644 --- a/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/NamingMethodPascal.cs +++ b/IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/NamingMethodPascal.cs @@ -95,9 +95,61 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context) return; } + // Skip test methods - they commonly use underscores for readability (e.g., "Method_Scenario_ExpectedResult") + if (IsTestMethod(namedTypeSymbol)) + { + return; + } + Diagnostic diagnostic = Diagnostic.Create(_Rule, namedTypeSymbol.Locations[0], name); context.ReportDiagnostic(diagnostic); } + + private static bool IsTestMethod(IMethodSymbol methodSymbol) + { + // Test framework namespaces - any method decorated with an attribute from these namespaces + // is considered a test method and exempt from PascalCase validation + string[] testFrameworkNamespaces = + [ + "Xunit", // xUnit (note: namespace is "Xunit", not "XUnit") + "NUnit.Framework", // NUnit + "Microsoft.VisualStudio.TestTools.UnitTesting", // MSTest + "TUnit.Core" // TUnit + ]; + + // Fallback attribute names - needed because our test infrastructure (DiagnosticVerifier) + // doesn't add references to test framework assemblies, so ContainingNamespace would be null + string[] commonTestAttributeNames = + [ + "TestMethod", "TestMethodAttribute", // MSTest + "Fact", "FactAttribute", // xUnit + "Theory", "TheoryAttribute", // xUnit + "Test", "TestAttribute", // NUnit + "TestCase", "TestCaseAttribute", // NUnit + "TestCaseSource", "TestCaseSourceAttribute" // NUnit + ]; + + ImmutableArray attributes = methodSymbol.GetAttributes(); + return attributes.Any(attribute => + { + if (attribute.AttributeClass == null) + { + return false; + } + + // Check namespace first (works in production with proper assembly references) + string containingNamespace = attribute.AttributeClass.ContainingNamespace?.ToDisplayString(); + if (containingNamespace != null && + testFrameworkNamespaces.Any(ns => containingNamespace.StartsWith(ns, StringComparison.Ordinal))) + { + return true; + } + + // Fallback: check attribute name (needed for test environment) + string attributeName = attribute.AttributeClass.Name; + return commonTestAttributeNames.Contains(attributeName); + }); + } } } diff --git a/docs/analyzers/00XX.Naming.md b/docs/analyzers/00XX.Naming.md index 2724c6be..7b822b8f 100644 --- a/docs/analyzers/00XX.Naming.md +++ b/docs/analyzers/00XX.Naming.md @@ -49,6 +49,8 @@ class SomeClass Methods, including local functions, should be PascalCase +**Note:** Test methods decorated with test framework attributes from xUnit, NUnit, MSTest, or TUnit are exempt from this rule, as they commonly use underscores for readability (e.g., `Method_Scenario_ExpectedResult`). Any attribute from these framework namespaces will be recognized automatically. + **Allowed** ```c# class SomeClass @@ -66,6 +68,19 @@ class SomeClass } ``` +**Allowed (Test Methods)** +```c# +[TestClass] +public class SomeClassTests +{ + [TestMethod] + public void GetEmpty_WhenCalled_ReturnsEmptyString() + { + // Test implementation + } +} +``` + **Disallowed** ```c# class SomeClass