Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<AttributeData> 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);
});
}
}
}
15 changes: 15 additions & 0 deletions docs/analyzers/00XX.Naming.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down