diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1649UnitTests.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1649UnitTests.cs index 040b76390..13dc67ade 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1649UnitTests.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1649UnitTests.cs @@ -43,22 +43,18 @@ public class SA1649UnitTests /// The type keyword to use during the test. /// A representing the asynchronous unit test. [Theory] - [MemberData(nameof(CommonMemberData.TypeDeclarationKeywords), MemberType = typeof(CommonMemberData))] + [MemberData(nameof(CommonMemberData.AllTypeDeclarationKeywords), MemberType = typeof(CommonMemberData))] public async Task VerifyWrongFileNameAsync(string typeKeyword) { var testCode = $@"namespace TestNamespace {{ - public {typeKeyword} {{|#0:TestType|}} - {{ - }} + {GetTypeDeclaration(typeKeyword, "TestType", diagnosticKey: 0)} }} "; var fixedCode = $@"namespace TestNamespace {{ - public {typeKeyword} TestType - {{ - }} + {GetTypeDeclaration(typeKeyword, "TestType")} }} "; @@ -73,22 +69,18 @@ public async Task VerifyWrongFileNameAsync(string typeKeyword) /// The type keyword to use during the test. /// A representing the asynchronous unit test. [Theory] - [MemberData(nameof(CommonMemberData.TypeDeclarationKeywords), MemberType = typeof(CommonMemberData))] + [MemberData(nameof(CommonMemberData.AllTypeDeclarationKeywords), MemberType = typeof(CommonMemberData))] public async Task VerifyWrongFileNameMultipleExtensionsAsync(string typeKeyword) { var testCode = $@"namespace TestNamespace {{ - public {typeKeyword} {{|#0:TestType|}} - {{ - }} + {GetTypeDeclaration(typeKeyword, "TestType", diagnosticKey: 0)} }} "; var fixedCode = $@"namespace TestNamespace {{ - public {typeKeyword} TestType - {{ - }} + {GetTypeDeclaration(typeKeyword, "TestType")} }} "; @@ -103,22 +95,18 @@ public async Task VerifyWrongFileNameMultipleExtensionsAsync(string typeKeyword) /// The type keyword to use during the test. /// A representing the asynchronous unit test. [Theory] - [MemberData(nameof(CommonMemberData.TypeDeclarationKeywords), MemberType = typeof(CommonMemberData))] + [MemberData(nameof(CommonMemberData.AllTypeDeclarationKeywords), MemberType = typeof(CommonMemberData))] public async Task VerifyWrongFileNameNoExtensionAsync(string typeKeyword) { var testCode = $@"namespace TestNamespace {{ - public {typeKeyword} {{|#0:TestType|}} - {{ - }} + {GetTypeDeclaration(typeKeyword, "TestType", diagnosticKey: 0)} }} "; var fixedCode = $@"namespace TestNamespace {{ - public {typeKeyword} TestType - {{ - }} + {GetTypeDeclaration(typeKeyword, "TestType")} }} "; @@ -132,14 +120,12 @@ public async Task VerifyWrongFileNameNoExtensionAsync(string typeKeyword) /// The type keyword to use during the test. /// A representing the asynchronous unit test. [Theory] - [MemberData(nameof(CommonMemberData.TypeDeclarationKeywords), MemberType = typeof(CommonMemberData))] + [MemberData(nameof(CommonMemberData.AllTypeDeclarationKeywords), MemberType = typeof(CommonMemberData))] public async Task VerifyCaseInsensitivityAsync(string typeKeyword) { var testCode = $@"namespace TestNamespace {{ - public {typeKeyword} TestType - {{ - }} + {GetTypeDeclaration(typeKeyword, "TestType")} }} "; @@ -157,17 +143,62 @@ public async Task VerifyFirstTypeIsUsedAsync(string typeKeyword) { var testCode = $@"namespace TestNamespace {{ - public {typeKeyword} TestType + public enum IgnoredEnum {{ }} + public delegate void IgnoredDelegate(); + + {GetTypeDeclaration(typeKeyword, "TestType", diagnosticKey: 0)} + + {GetTypeDeclaration(typeKeyword, "TestType2")} +}} +"; + var fixedCode = $@"namespace TestNamespace +{{ + public enum IgnoredEnum {{ }} + public delegate void IgnoredDelegate(); + + {GetTypeDeclaration(typeKeyword, "TestType")} + + {GetTypeDeclaration(typeKeyword, "TestType2")} +}} +"; + + var expectedDiagnostic = Diagnostic().WithLocation(0); + await VerifyCSharpFixAsync("TestType2.cs", testCode, StyleCopSettings, expectedDiagnostic, "TestType.cs", fixedCode, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + [WorkItem(3234, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3234")] + public async Task VerifyMultipleEnumTypesIgnoredAsync() + { + var testCode = $@"namespace TestNamespace +{{ + public enum TestType {{ }} - public {typeKeyword} TestType2 + public enum TestType2 {{ }} }} "; - await VerifyCSharpDiagnosticAsync("TestType.cs", testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + // File names are not checked for 'enum' if more than one is present + await VerifyCSharpDiagnosticAsync("TestType2.cs", testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + [WorkItem(3234, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3234")] + public async Task VerifyMultipleDelegateTypesIgnoredAsync() + { + var testCode = $@"namespace TestNamespace +{{ + public delegate void TestType(); + public delegate void TestType2(); +}} +"; + + // File names are not checked for 'delegate' if more than one is present + await VerifyCSharpDiagnosticAsync("TestType2.cs", testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); } /// @@ -281,6 +312,20 @@ public async Task VerifyWithoutFirstTypeAsync() await VerifyCSharpDiagnosticAsync("Test0.cs", testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); } + private static string GetTypeDeclaration(string typeKind, string typeName, int? diagnosticKey = null) + { + if (diagnosticKey is not null) + { + typeName = $"{{|#{diagnosticKey}:{typeName}|}}"; + } + + return typeKind switch + { + "delegate" => $"public delegate void {typeName}();", + _ => $"public {typeKind} {typeName} {{ }}", + }; + } + private static Task VerifyCSharpDiagnosticAsync(string fileName, string source, DiagnosticResult[] expected, CancellationToken cancellationToken) { var test = new StyleCopCodeFixVerifier.CSharpTest() diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/Helpers/CommonMemberData.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/Helpers/CommonMemberData.cs index 92f3d655d..eb74fdfc0 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/Helpers/CommonMemberData.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/Helpers/CommonMemberData.cs @@ -39,5 +39,14 @@ public static IEnumerable BaseTypeDeclarationKeywords .Concat(new[] { new[] { "enum" } }); } } + + public static IEnumerable AllTypeDeclarationKeywords + { + get + { + return BaseTypeDeclarationKeywords + .Concat(new[] { new[] { "delegate" } }); + } + } } } diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1649FileNameMustMatchTypeName.cs b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1649FileNameMustMatchTypeName.cs index f4fd14256..125fc6e4d 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1649FileNameMustMatchTypeName.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1649FileNameMustMatchTypeName.cs @@ -67,7 +67,8 @@ public static void HandleSyntaxTree(SyntaxTreeAnalysisContext context, StyleCopS return; } - if (firstTypeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword)) + var modifiers = (firstTypeDeclaration as BaseTypeDeclarationSyntax)?.Modifiers ?? SyntaxFactory.TokenList(); + if (modifiers.Any(SyntaxKind.PartialKeyword)) { return; } @@ -87,15 +88,31 @@ public static void HandleSyntaxTree(SyntaxTreeAnalysisContext context, StyleCopS var properties = ImmutableDictionary.Create() .Add(ExpectedFileNameKey, expectedFileName + suffix); - context.ReportDiagnostic(Diagnostic.Create(Descriptor, firstTypeDeclaration.Identifier.GetLocation(), properties)); + var identifier = (firstTypeDeclaration as BaseTypeDeclarationSyntax)?.Identifier + ?? ((DelegateDeclarationSyntax)firstTypeDeclaration).Identifier; + context.ReportDiagnostic(Diagnostic.Create(Descriptor, identifier.GetLocation(), properties)); } } - private static TypeDeclarationSyntax GetFirstTypeDeclaration(SyntaxNode root) + private static MemberDeclarationSyntax GetFirstTypeDeclaration(SyntaxNode root) { - return root.DescendantNodes(descendIntoChildren: node => node.IsKind(SyntaxKind.CompilationUnit) || node.IsKind(SyntaxKind.NamespaceDeclaration)) + // Prefer to find the first type which is a true TypeDeclarationSyntax + MemberDeclarationSyntax firstTypeDeclaration = root.DescendantNodes(descendIntoChildren: node => node.IsKind(SyntaxKind.CompilationUnit) || node.IsKind(SyntaxKind.NamespaceDeclaration)) .OfType() .FirstOrDefault(); + + // If no TypeDeclarationSyntax is found, expand the search to any type declaration as long as only one + // is present + var expandedTypeDeclarations = root.DescendantNodes(descendIntoChildren: node => node.IsKind(SyntaxKind.CompilationUnit) || node.IsKind(SyntaxKind.NamespaceDeclaration)) + .OfType() + .Where(node => node is BaseTypeDeclarationSyntax || node.IsKind(SyntaxKind.DelegateDeclaration)) + .ToList(); + if (expandedTypeDeclarations.Count == 1) + { + firstTypeDeclaration = expandedTypeDeclarations[0]; + } + + return firstTypeDeclaration; } } }