Skip to content

Commit 267e87f

Browse files
Use real framework assemblies and data-driven tests for INTL0003 exemption
- Add xunit.v3.core, NUnit, TUnit.Core as compile+runtime references in test project (PrivateAssets=all so no runner conflicts; IncludeAssets=compile;runtime for DLL availability) - Create TestFrameworkReferences registry: single source of truth for all supported test frameworks (name, attribute syntax, using directive, MetadataReference). Adding a new framework only requires one entry here + the namespace in NamingMethodPascal. - Inject all framework references into every DiagnosticVerifier.Helper CreateProject compilation, so namespace-based detection is exercised by real tests (not just fallback) - Collapse 4 redundant framework-specific test methods into one [DynamicData] test driven by TestFrameworkReferences.All; new frameworks auto-covered at zero cost - Add negative test: non-test method with underscores in a test class still warns - Add user-defined [Test] attribute test: custom namespace attribute still warns - Fix IsTestMethod fallback: gate name-based matching on TypeKind.Error only, preventing false negatives for user-defined attributes named Test/Fact/Theory/etc. - Make testFrameworkNamespaces and commonTestAttributeNames static readonly fields to avoid per-invocation heap allocation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent fa9196d commit 267e87f

File tree

6 files changed

+166
-82
lines changed

6 files changed

+166
-82
lines changed

Directory.Packages.props

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,9 @@
1616
<PackageVersion Include="MSTest.TestAdapter" Version="4.1.0" />
1717
<PackageVersion Include="MSTest.TestFramework" Version="4.1.0" />
1818
<PackageVersion Include="System.CommandLine.DragonFruit" Version="0.4.0-alpha.23407.1" />
19+
<!-- Reference-only packages for test framework namespace resolution in analyzer tests -->
20+
<PackageVersion Include="xunit.v3.core" Version="3.2.2" />
21+
<PackageVersion Include="NUnit" Version="4.5.1" />
22+
<PackageVersion Include="TUnit.Core" Version="1.19.22" />
1923
</ItemGroup>
2024
</Project>

IntelliTect.Analyzer/IntelliTect.Analyzer.Test/Helpers/DiagnosticVerifier.Helper.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,11 @@ private static Project CreateProject(string[] sources, string language = Languag
184184
{
185185
solution = solution.AddMetadataReference(projectId, _SystemRuntimeReference);
186186
}
187+
188+
foreach (MetadataReference frameworkReference in TestFrameworkReferences.AllReferences)
189+
{
190+
solution = solution.AddMetadataReference(projectId, frameworkReference);
191+
}
187192
}
188193

189194
int count = 0;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using Microsoft.CodeAnalysis;
4+
5+
namespace TestHelper
6+
{
7+
/// <summary>
8+
/// Registry of supported test frameworks for analyzer testing.
9+
/// Used both to inject real assembly references into in-memory Roslyn compilations
10+
/// and as data source for parameterized tests via [DynamicData].
11+
///
12+
/// To add a new test framework: add one entry to <see cref="All"/>,
13+
/// add the NuGet package to Directory.Packages.props and the test .csproj
14+
/// (with IncludeAssets="compile" PrivateAssets="all"), and add the namespace
15+
/// to <see cref="IntelliTect.Analyzer.Analyzers.NamingMethodPascal"/>'s namespace list.
16+
/// </summary>
17+
public static class TestFrameworkReferences
18+
{
19+
public record TestFramework(
20+
string Name,
21+
string TestAttribute,
22+
string UsingDirective,
23+
MetadataReference Reference);
24+
25+
public static IReadOnlyList<TestFramework> All { get; } =
26+
[
27+
new("MSTest",
28+
"[TestMethod]",
29+
"using Microsoft.VisualStudio.TestTools.UnitTesting;",
30+
MetadataReference.CreateFromFile(
31+
typeof(Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute).Assembly.Location)),
32+
new("xUnit",
33+
"[Fact]",
34+
"using Xunit;",
35+
MetadataReference.CreateFromFile(
36+
typeof(Xunit.FactAttribute).Assembly.Location)),
37+
new("NUnit",
38+
"[Test]",
39+
"using NUnit.Framework;",
40+
MetadataReference.CreateFromFile(
41+
typeof(NUnit.Framework.TestAttribute).Assembly.Location)),
42+
new("TUnit",
43+
"[Test]",
44+
"using TUnit.Core;",
45+
MetadataReference.CreateFromFile(
46+
typeof(TUnit.Core.TestAttribute).Assembly.Location)),
47+
];
48+
49+
/// <summary>All test framework assembly references, for use in <see cref="DiagnosticVerifier"/>.</summary>
50+
public static MetadataReference[] AllReferences =>
51+
[.. All.Select(f => f.Reference)];
52+
}
53+
}

IntelliTect.Analyzer/IntelliTect.Analyzer.Test/IntelliTect.Analyzer.Tests.csproj

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@
1818
<PackageReference Include="Microsoft.NET.Test.Sdk" />
1919
<PackageReference Include="MSTest.TestAdapter" />
2020
<PackageReference Include="MSTest.TestFramework" />
21+
<!-- Reference-only: provides DLLs for MetadataReference in tests; no runner/analyzer assets -->
22+
<PackageReference Include="xunit.v3.core">
23+
<IncludeAssets>compile;runtime</IncludeAssets>
24+
<PrivateAssets>all</PrivateAssets>
25+
</PackageReference>
26+
<PackageReference Include="NUnit">
27+
<IncludeAssets>compile;runtime</IncludeAssets>
28+
<PrivateAssets>all</PrivateAssets>
29+
</PackageReference>
30+
<PackageReference Include="TUnit.Core">
31+
<IncludeAssets>compile;runtime</IncludeAssets>
32+
<PrivateAssets>all</PrivateAssets>
33+
</PackageReference>
2134
</ItemGroup>
2235

2336
<ItemGroup>

IntelliTect.Analyzer/IntelliTect.Analyzer.Test/NamingMethodPascalTests.cs

Lines changed: 57 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using System.Threading.Tasks;
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Threading.Tasks;
24
using Microsoft.CodeAnalysis;
35
using Microsoft.CodeAnalysis.CodeFixes;
46
using Microsoft.CodeAnalysis.Diagnostics;
@@ -442,33 +444,35 @@ void IInterface.foo() { }
442444
VerifyCSharpDiagnostic(test, expected1, expected2);
443445
}
444446

447+
public static IEnumerable<object[]> TestFrameworks =>
448+
TestFrameworkReferences.All.Select(f => new object[] { f.Name, f.TestAttribute, f.UsingDirective });
449+
445450
[TestMethod]
446-
[Description("Test method with underscores should not trigger INTL0003")]
447-
public void TestMethodWithUnderscores_MSTest_NoDiagnosticInformationReturned()
451+
[DynamicData(nameof(TestFrameworks))]
452+
[Description("Issue 371 - Test method with underscores should not trigger INTL0003 for any supported framework")]
453+
public void TestMethodWithUnderscores_TestFrameworkAttribute_NoDiagnosticInformationReturned(
454+
string frameworkName, string testAttribute, string usingDirective)
448455
{
449-
string test = @"
456+
_ = frameworkName; // parameter used for test identification in output
457+
string test = $@"
450458
using System;
451-
using Microsoft.VisualStudio.TestTools.UnitTesting;
459+
{usingDirective}
452460
453461
namespace ConsoleApplication1
454-
{
455-
[TestClass]
462+
{{
456463
public class TypeName
457-
{
458-
[TestMethod]
459-
public void FooThing_IsFooThing_HasFooThing()
460-
{
461-
Assert.IsTrue(true);
462-
}
463-
}
464-
}";
464+
{{
465+
{testAttribute}
466+
public void FooThing_IsFooThing_HasFooThing() {{ }}
467+
}}
468+
}}";
465469

466470
VerifyCSharpDiagnostic(test);
467471
}
468472

469473
[TestMethod]
470-
[Description("Test method with underscores should not trigger INTL0003 - xUnit Fact")]
471-
public void TestMethodWithUnderscores_XunitFact_NoDiagnosticInformationReturned()
474+
[Description("Issue 371 - Non-test method with underscores in a test class should still trigger INTL0003")]
475+
public void NonTestMethodWithUnderscores_InTestClass_DiagnosticReturned()
472476
{
473477
string test = @"
474478
using System;
@@ -477,62 +481,61 @@ public void TestMethodWithUnderscores_XunitFact_NoDiagnosticInformationReturned(
477481
namespace ConsoleApplication1
478482
{
479483
public class TypeName
480-
{
484+
{
481485
[Fact]
482-
public void FooThing_IsFooThing_HasFooThing()
483-
{
484-
Assert.True(true);
485-
}
486-
}
487-
}";
486+
public void FooThing_IsFooThing_HasFooThing() { }
488487
489-
VerifyCSharpDiagnostic(test);
488+
public void helper_setup() { }
490489
}
490+
}";
491491

492-
[TestMethod]
493-
[Description("Test method with underscores should not trigger INTL0003 - xUnit Theory")]
494-
public void TestMethodWithUnderscores_XunitTheory_NoDiagnosticInformationReturned()
495-
{
496-
string test = @"
497-
using System;
498-
using Xunit;
499-
500-
namespace ConsoleApplication1
501-
{
502-
public class TypeName
503-
{
504-
[Theory]
505-
public void FooThing_IsFooThing_HasFooThing()
492+
var expected = new DiagnosticResult
506493
{
507-
Assert.True(true);
508-
}
509-
}
510-
}";
494+
Id = "INTL0003",
495+
Message = "Method 'helper_setup' should be PascalCase",
496+
Severity = DiagnosticSeverity.Warning,
497+
Locations =
498+
[
499+
new DiagnosticResultLocation("Test0.cs", 12, 25)
500+
]
501+
};
511502

512-
VerifyCSharpDiagnostic(test);
503+
VerifyCSharpDiagnostic(test, expected);
513504
}
514505

515506
[TestMethod]
516-
[Description("Test method with underscores should not trigger INTL0003 - NUnit Test")]
517-
public void TestMethodWithUnderscores_NUnitTest_NoDiagnosticInformationReturned()
507+
[Description("Issue 371 - User-defined attribute named 'Test' in a non-framework namespace must not suppress INTL0003")]
508+
public void MethodWithUnderscores_UserDefinedTestAttribute_DiagnosticReturned()
518509
{
519510
string test = @"
520511
using System;
521-
using NUnit.Framework;
512+
513+
namespace MyApp
514+
{
515+
public class TestAttribute : System.Attribute { }
516+
}
522517
523518
namespace ConsoleApplication1
524519
{
525520
public class TypeName
526-
{
527-
[Test]
528-
public void FooThing_IsFooThing_HasFooThing()
529-
{
530-
Assert.That(true, Is.True);
531-
}
521+
{
522+
[MyApp.Test]
523+
public void FooThing_IsFooThing_HasFooThing() { }
532524
}
533525
}";
534526

535-
VerifyCSharpDiagnostic(test);
527+
var expected = new DiagnosticResult
528+
{
529+
Id = "INTL0003",
530+
Message = "Method 'FooThing_IsFooThing_HasFooThing' should be PascalCase",
531+
Severity = DiagnosticSeverity.Warning,
532+
Locations =
533+
[
534+
new DiagnosticResultLocation("Test0.cs", 14, 25)
535+
]
536+
};
537+
538+
VerifyCSharpDiagnostic(test, expected);
536539
}
537540

538541

IntelliTect.Analyzer/IntelliTect.Analyzer/Analyzers/NamingMethodPascal.cs

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -105,30 +105,31 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context)
105105
context.ReportDiagnostic(diagnostic);
106106
}
107107

108+
// Test framework namespaces — any method decorated with an attribute from these namespaces
109+
// is considered a test method and exempt from PascalCase validation.
110+
// To add a new framework, append its root namespace here and update TestFrameworkReferences.cs.
111+
private static readonly string[] s_testFrameworkNamespaces =
112+
[
113+
"Xunit", // xUnit (namespace is "Xunit", not "XUnit")
114+
"NUnit.Framework", // NUnit
115+
"Microsoft.VisualStudio.TestTools.UnitTesting", // MSTest
116+
"TUnit.Core" // TUnit
117+
];
118+
119+
// Fallback attribute names for compilations without framework assembly references.
120+
// Only used when the attribute type is unresolved (TypeKind.Error).
121+
private static readonly string[] s_commonTestAttributeNames =
122+
[
123+
"TestMethod", "TestMethodAttribute", // MSTest
124+
"Fact", "FactAttribute", // xUnit
125+
"Theory", "TheoryAttribute", // xUnit
126+
"Test", "TestAttribute", // NUnit / TUnit
127+
"TestCase", "TestCaseAttribute", // NUnit
128+
"TestCaseSource", "TestCaseSourceAttribute" // NUnit
129+
];
130+
108131
private static bool IsTestMethod(IMethodSymbol methodSymbol)
109132
{
110-
// Test framework namespaces - any method decorated with an attribute from these namespaces
111-
// is considered a test method and exempt from PascalCase validation
112-
string[] testFrameworkNamespaces =
113-
[
114-
"Xunit", // xUnit (note: namespace is "Xunit", not "XUnit")
115-
"NUnit.Framework", // NUnit
116-
"Microsoft.VisualStudio.TestTools.UnitTesting", // MSTest
117-
"TUnit.Core" // TUnit
118-
];
119-
120-
// Fallback attribute names - needed because our test infrastructure (DiagnosticVerifier)
121-
// doesn't add references to test framework assemblies, so ContainingNamespace would be null
122-
string[] commonTestAttributeNames =
123-
[
124-
"TestMethod", "TestMethodAttribute", // MSTest
125-
"Fact", "FactAttribute", // xUnit
126-
"Theory", "TheoryAttribute", // xUnit
127-
"Test", "TestAttribute", // NUnit
128-
"TestCase", "TestCaseAttribute", // NUnit
129-
"TestCaseSource", "TestCaseSourceAttribute" // NUnit
130-
];
131-
132133
ImmutableArray<AttributeData> attributes = methodSymbol.GetAttributes();
133134
return attributes.Any(attribute =>
134135
{
@@ -137,17 +138,22 @@ private static bool IsTestMethod(IMethodSymbol methodSymbol)
137138
return false;
138139
}
139140

140-
// Check namespace first (works in production with proper assembly references)
141+
// Check namespace first works whenever the compilation includes real framework references.
141142
string containingNamespace = attribute.AttributeClass.ContainingNamespace?.ToDisplayString();
142-
if (containingNamespace != null &&
143-
testFrameworkNamespaces.Any(ns => containingNamespace.StartsWith(ns, StringComparison.Ordinal)))
143+
if (containingNamespace != null &&
144+
s_testFrameworkNamespaces.Any(ns => containingNamespace.StartsWith(ns, StringComparison.Ordinal)))
144145
{
145146
return true;
146147
}
147148

148-
// Fallback: check attribute name (needed for test environment)
149-
string attributeName = attribute.AttributeClass.Name;
150-
return commonTestAttributeNames.Contains(attributeName);
149+
// Fallback: check by name only for unresolved types (missing assembly reference in compilation).
150+
// Gated on TypeKind.Error to avoid false negatives for user-defined attributes with the same names.
151+
if (attribute.AttributeClass.TypeKind == TypeKind.Error)
152+
{
153+
return s_commonTestAttributeNames.Contains(attribute.AttributeClass.Name);
154+
}
155+
156+
return false;
151157
});
152158
}
153159
}

0 commit comments

Comments
 (0)