Skip to content

Commit 37a0ba2

Browse files
committed
Check file name for single 'enum' and 'delegate' top-level types
Closes #3234
1 parent 8056de6 commit 37a0ba2

File tree

3 files changed

+103
-32
lines changed

3 files changed

+103
-32
lines changed

StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1649UnitTests.cs

Lines changed: 73 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -43,22 +43,18 @@ public class SA1649UnitTests
4343
/// <param name="typeKeyword">The type keyword to use during the test.</param>
4444
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
4545
[Theory]
46-
[MemberData(nameof(CommonMemberData.TypeDeclarationKeywords), MemberType = typeof(CommonMemberData))]
46+
[MemberData(nameof(CommonMemberData.AllTypeDeclarationKeywords), MemberType = typeof(CommonMemberData))]
4747
public async Task VerifyWrongFileNameAsync(string typeKeyword)
4848
{
4949
var testCode = $@"namespace TestNamespace
5050
{{
51-
public {typeKeyword} {{|#0:TestType|}}
52-
{{
53-
}}
51+
{GetTypeDeclaration(typeKeyword, "TestType", diagnosticKey: 0)}
5452
}}
5553
";
5654

5755
var fixedCode = $@"namespace TestNamespace
5856
{{
59-
public {typeKeyword} TestType
60-
{{
61-
}}
57+
{GetTypeDeclaration(typeKeyword, "TestType")}
6258
}}
6359
";
6460

@@ -73,22 +69,18 @@ public async Task VerifyWrongFileNameAsync(string typeKeyword)
7369
/// <param name="typeKeyword">The type keyword to use during the test.</param>
7470
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
7571
[Theory]
76-
[MemberData(nameof(CommonMemberData.TypeDeclarationKeywords), MemberType = typeof(CommonMemberData))]
72+
[MemberData(nameof(CommonMemberData.AllTypeDeclarationKeywords), MemberType = typeof(CommonMemberData))]
7773
public async Task VerifyWrongFileNameMultipleExtensionsAsync(string typeKeyword)
7874
{
7975
var testCode = $@"namespace TestNamespace
8076
{{
81-
public {typeKeyword} {{|#0:TestType|}}
82-
{{
83-
}}
77+
{GetTypeDeclaration(typeKeyword, "TestType", diagnosticKey: 0)}
8478
}}
8579
";
8680

8781
var fixedCode = $@"namespace TestNamespace
8882
{{
89-
public {typeKeyword} TestType
90-
{{
91-
}}
83+
{GetTypeDeclaration(typeKeyword, "TestType")}
9284
}}
9385
";
9486

@@ -103,22 +95,18 @@ public async Task VerifyWrongFileNameMultipleExtensionsAsync(string typeKeyword)
10395
/// <param name="typeKeyword">The type keyword to use during the test.</param>
10496
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
10597
[Theory]
106-
[MemberData(nameof(CommonMemberData.TypeDeclarationKeywords), MemberType = typeof(CommonMemberData))]
98+
[MemberData(nameof(CommonMemberData.AllTypeDeclarationKeywords), MemberType = typeof(CommonMemberData))]
10799
public async Task VerifyWrongFileNameNoExtensionAsync(string typeKeyword)
108100
{
109101
var testCode = $@"namespace TestNamespace
110102
{{
111-
public {typeKeyword} {{|#0:TestType|}}
112-
{{
113-
}}
103+
{GetTypeDeclaration(typeKeyword, "TestType", diagnosticKey: 0)}
114104
}}
115105
";
116106

117107
var fixedCode = $@"namespace TestNamespace
118108
{{
119-
public {typeKeyword} TestType
120-
{{
121-
}}
109+
{GetTypeDeclaration(typeKeyword, "TestType")}
122110
}}
123111
";
124112

@@ -132,14 +120,12 @@ public async Task VerifyWrongFileNameNoExtensionAsync(string typeKeyword)
132120
/// <param name="typeKeyword">The type keyword to use during the test.</param>
133121
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
134122
[Theory]
135-
[MemberData(nameof(CommonMemberData.TypeDeclarationKeywords), MemberType = typeof(CommonMemberData))]
123+
[MemberData(nameof(CommonMemberData.AllTypeDeclarationKeywords), MemberType = typeof(CommonMemberData))]
136124
public async Task VerifyCaseInsensitivityAsync(string typeKeyword)
137125
{
138126
var testCode = $@"namespace TestNamespace
139127
{{
140-
public {typeKeyword} TestType
141-
{{
142-
}}
128+
{GetTypeDeclaration(typeKeyword, "TestType")}
143129
}}
144130
";
145131

@@ -157,17 +143,62 @@ public async Task VerifyFirstTypeIsUsedAsync(string typeKeyword)
157143
{
158144
var testCode = $@"namespace TestNamespace
159145
{{
160-
public {typeKeyword} TestType
146+
public enum IgnoredEnum {{ }}
147+
public delegate void IgnoredDelegate();
148+
149+
{GetTypeDeclaration(typeKeyword, "TestType", diagnosticKey: 0)}
150+
151+
{GetTypeDeclaration(typeKeyword, "TestType2")}
152+
}}
153+
";
154+
var fixedCode = $@"namespace TestNamespace
155+
{{
156+
public enum IgnoredEnum {{ }}
157+
public delegate void IgnoredDelegate();
158+
159+
{GetTypeDeclaration(typeKeyword, "TestType")}
160+
161+
{GetTypeDeclaration(typeKeyword, "TestType2")}
162+
}}
163+
";
164+
165+
var expectedDiagnostic = Diagnostic().WithLocation(0);
166+
await VerifyCSharpFixAsync("TestType2.cs", testCode, StyleCopSettings, expectedDiagnostic, "TestType.cs", fixedCode, CancellationToken.None).ConfigureAwait(false);
167+
}
168+
169+
[Fact]
170+
[WorkItem(3234, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3234")]
171+
public async Task VerifyMultipleEnumTypesIgnoredAsync()
172+
{
173+
var testCode = $@"namespace TestNamespace
174+
{{
175+
public enum TestType
161176
{{
162177
}}
163178
164-
public {typeKeyword} TestType2
179+
public enum TestType2
165180
{{
166181
}}
167182
}}
168183
";
169184

170-
await VerifyCSharpDiagnosticAsync("TestType.cs", testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
185+
// File names are not checked for 'enum' if more than one is present
186+
await VerifyCSharpDiagnosticAsync("TestType2.cs", testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
187+
}
188+
189+
[Fact]
190+
[WorkItem(3234, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3234")]
191+
public async Task VerifyMultipleDelegateTypesIgnoredAsync()
192+
{
193+
var testCode = $@"namespace TestNamespace
194+
{{
195+
public delegate void TestType();
196+
public delegate void TestType2();
197+
}}
198+
";
199+
200+
// File names are not checked for 'delegate' if more than one is present
201+
await VerifyCSharpDiagnosticAsync("TestType2.cs", testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
171202
}
172203

173204
/// <summary>
@@ -281,6 +312,20 @@ public async Task VerifyWithoutFirstTypeAsync()
281312
await VerifyCSharpDiagnosticAsync("Test0.cs", testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
282313
}
283314

315+
private static string GetTypeDeclaration(string typeKind, string typeName, int? diagnosticKey = null)
316+
{
317+
if (diagnosticKey is not null)
318+
{
319+
typeName = $"{{|#{diagnosticKey}:{typeName}|}}";
320+
}
321+
322+
return typeKind switch
323+
{
324+
"delegate" => $"public delegate void {typeName}();",
325+
_ => $"public {typeKind} {typeName} {{ }}",
326+
};
327+
}
328+
284329
private static Task VerifyCSharpDiagnosticAsync(string fileName, string source, DiagnosticResult[] expected, CancellationToken cancellationToken)
285330
{
286331
var test = new StyleCopCodeFixVerifier<SA1649FileNameMustMatchTypeName, SA1649CodeFixProvider>.CSharpTest()

StyleCop.Analyzers/StyleCop.Analyzers.Test/Helpers/CommonMemberData.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,14 @@ public static IEnumerable<object[]> BaseTypeDeclarationKeywords
3939
.Concat(new[] { new[] { "enum" } });
4040
}
4141
}
42+
43+
public static IEnumerable<object[]> AllTypeDeclarationKeywords
44+
{
45+
get
46+
{
47+
return BaseTypeDeclarationKeywords
48+
.Concat(new[] { new[] { "delegate" } });
49+
}
50+
}
4251
}
4352
}

StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1649FileNameMustMatchTypeName.cs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ public static void HandleSyntaxTree(SyntaxTreeAnalysisContext context, StyleCopS
6767
return;
6868
}
6969

70-
if (firstTypeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword))
70+
var modifiers = (firstTypeDeclaration as BaseTypeDeclarationSyntax)?.Modifiers ?? SyntaxFactory.TokenList();
71+
if (modifiers.Any(SyntaxKind.PartialKeyword))
7172
{
7273
return;
7374
}
@@ -87,15 +88,31 @@ public static void HandleSyntaxTree(SyntaxTreeAnalysisContext context, StyleCopS
8788
var properties = ImmutableDictionary.Create<string, string>()
8889
.Add(ExpectedFileNameKey, expectedFileName + suffix);
8990

90-
context.ReportDiagnostic(Diagnostic.Create(Descriptor, firstTypeDeclaration.Identifier.GetLocation(), properties));
91+
var identifier = (firstTypeDeclaration as BaseTypeDeclarationSyntax)?.Identifier
92+
?? ((DelegateDeclarationSyntax)firstTypeDeclaration).Identifier;
93+
context.ReportDiagnostic(Diagnostic.Create(Descriptor, identifier.GetLocation(), properties));
9194
}
9295
}
9396

94-
private static TypeDeclarationSyntax GetFirstTypeDeclaration(SyntaxNode root)
97+
private static MemberDeclarationSyntax GetFirstTypeDeclaration(SyntaxNode root)
9598
{
96-
return root.DescendantNodes(descendIntoChildren: node => node.IsKind(SyntaxKind.CompilationUnit) || node.IsKind(SyntaxKind.NamespaceDeclaration))
99+
// Prefer to find the first type which is a true TypeDeclarationSyntax
100+
MemberDeclarationSyntax firstTypeDeclaration = root.DescendantNodes(descendIntoChildren: node => node.IsKind(SyntaxKind.CompilationUnit) || node.IsKind(SyntaxKind.NamespaceDeclaration))
97101
.OfType<TypeDeclarationSyntax>()
98102
.FirstOrDefault();
103+
104+
// If no TypeDeclarationSyntax is found, expand the search to any type declaration as long as only one
105+
// is present
106+
var expandedTypeDeclarations = root.DescendantNodes(descendIntoChildren: node => node.IsKind(SyntaxKind.CompilationUnit) || node.IsKind(SyntaxKind.NamespaceDeclaration))
107+
.OfType<MemberDeclarationSyntax>()
108+
.Where(node => node is BaseTypeDeclarationSyntax || node.IsKind(SyntaxKind.DelegateDeclaration))
109+
.ToList();
110+
if (expandedTypeDeclarations.Count == 1)
111+
{
112+
firstTypeDeclaration = expandedTypeDeclarations[0];
113+
}
114+
115+
return firstTypeDeclaration;
99116
}
100117
}
101118
}

0 commit comments

Comments
 (0)