From cfa7ab74ad7b31a49efcd467a585e39853f2706b Mon Sep 17 00:00:00 2001 From: Alexander Batishchev Date: Tue, 10 Oct 2023 18:58:14 -0700 Subject: [PATCH] Added support for excluded_symbol_names to FileNameMustMatchTypeNameAnalyzer (MA0048) (#611) --- docs/Rules/MA0048.md | 3 + .../FileNameMustMatchTypeNameAnalyzer.cs | 41 ++++++++++- .../Helpers/ProjectBuilder.cs | 52 ++++++-------- .../FileNameMustMatchTypeNameAnalyzerTests.cs | 72 +++++++++++++++++++ 4 files changed, 133 insertions(+), 35 deletions(-) diff --git a/docs/Rules/MA0048.md b/docs/Rules/MA0048.md index 4cdb3f6e1..b22f3c569 100644 --- a/docs/Rules/MA0048.md +++ b/docs/Rules/MA0048.md @@ -62,4 +62,7 @@ public class DoNotMatchFileName3 { } # Only validate the first type in a file. default: false MA0048.only_validate_first_type = false + +# Ignore certain types in a file. default: none +dotnet_diagnostic.MA0048.excluded_symbol_names = Foo|T:MyNamespace.Bar ```` diff --git a/src/Meziantou.Analyzer/Rules/FileNameMustMatchTypeNameAnalyzer.cs b/src/Meziantou.Analyzer/Rules/FileNameMustMatchTypeNameAnalyzer.cs index b60493e2b..292c01125 100644 --- a/src/Meziantou.Analyzer/Rules/FileNameMustMatchTypeNameAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/FileNameMustMatchTypeNameAnalyzer.cs @@ -2,6 +2,7 @@ using System.Collections.Immutable; using System.Globalization; using System.Linq; +using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -44,7 +45,7 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context) continue; // Nested type - if (symbol.ContainingType != null) + if (symbol.ContainingType is not null) continue; #if ROSLYN_4_4_OR_GREATER @@ -52,6 +53,28 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context) continue; #endif + var symbolName = symbol.Name; + + // dotnet_diagnostic.MA0048.excluded_symbol_names + var excludedSymbolNames = context.Options.GetConfigurationValue(location.SourceTree, "dotnet_diagnostic." + s_rule.Id + ".excluded_symbol_names", defaultValue: string.Empty); + if (!string.IsNullOrEmpty(excludedSymbolNames)) + { + var symbolDeclarationId = DocumentationCommentId.CreateDeclarationId(symbol); + var excludedSymbolNamesSplit = excludedSymbolNames.Split('|', StringSplitOptions.RemoveEmptyEntries); + var matched = false; + + foreach (var excludedSymbolName in excludedSymbolNamesSplit) + { + if (IsWildcardMatch(symbolName, excludedSymbolName) || IsWildcardMatch(symbolDeclarationId, excludedSymbolName)) + matched = true; + } + + // to continue the outer foreach loop + if (matched) + continue; + } + + // MA0048.only_validate_first_type if (context.Options.GetConfigurationValue(location.SourceTree, s_rule.Id + ".only_validate_first_type", defaultValue: false)) { var root = location.SourceTree.GetRoot(context.CancellationToken); @@ -74,8 +97,8 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context) } var filePath = location.SourceTree.FilePath; - var fileName = filePath == null ? null : GetFileName(filePath.AsSpan()); - var symbolName = symbol.Name; + var fileName = filePath is not null ? GetFileName(filePath.AsSpan()) : null; + if (fileName.Equals(symbolName.AsSpan(), StringComparison.OrdinalIgnoreCase)) continue; @@ -115,4 +138,16 @@ private static ReadOnlySpan GetFileName(ReadOnlySpan filePath) return filePath[..index]; } + + /// + /// Implemented wildcard pattern match + /// + /// + /// Would match FooManager for expression *Manager + /// + private static bool IsWildcardMatch(string input, string pattern) + { + var wildcardPattern = $"^{Regex.Escape(pattern).Replace("\\*", ".*", StringComparison.Ordinal)}$"; + return Regex.IsMatch(input, wildcardPattern, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase, TimeSpan.FromSeconds(1)); + } } diff --git a/tests/Meziantou.Analyzer.Test/Helpers/ProjectBuilder.cs b/tests/Meziantou.Analyzer.Test/Helpers/ProjectBuilder.cs index dec7d0198..58f079980 100644 --- a/tests/Meziantou.Analyzer.Test/Helpers/ProjectBuilder.cs +++ b/tests/Meziantou.Analyzer.Test/Helpers/ProjectBuilder.cs @@ -46,10 +46,9 @@ public sealed partial class ProjectBuilder private static Task GetNuGetReferences(string packageName, string version, params string[] paths) { - var task = s_cache.GetOrAdd(packageName + '@' + version + ':' + string.Join(",", paths), key => - { - return new Lazy>(Download); - }); + var task = s_cache.GetOrAdd( + packageName + '@' + version + ':' + string.Join(",", paths), + _ => new Lazy>(Download)); return task.Value; @@ -126,10 +125,12 @@ public ProjectBuilder WithAnalyzerFromNuGet(string packageName, string version, return this; } - public ProjectBuilder WithMicrosoftCodeAnalysisNetAnalyzers(params string[] ruleIds) - { - return WithAnalyzerFromNuGet("Microsoft.CodeAnalysis.NetAnalyzers", "7.0.1", paths: new[] { "analyzers/dotnet/cs/Microsoft.CodeAnalysis" }, ruleIds); - } + public ProjectBuilder WithMicrosoftCodeAnalysisNetAnalyzers(params string[] ruleIds) => + WithAnalyzerFromNuGet( + "Microsoft.CodeAnalysis.NetAnalyzers", + "7.0.1", + paths: new[] { "analyzers/dotnet/cs/Microsoft.CodeAnalysis" }, + ruleIds); public ProjectBuilder AddMSTestApi() => AddNuGetReference("MSTest.TestFramework", "2.1.1", "lib/netstandard1.0/"); @@ -152,10 +153,8 @@ public ProjectBuilder WithOutputKind(OutputKind outputKind) return this; } - public ProjectBuilder WithSourceCode(string sourceCode) - { - return WithSourceCode(fileName: null, sourceCode); - } + public ProjectBuilder WithSourceCode(string sourceCode) => + WithSourceCode(fileName: null, sourceCode); public ProjectBuilder WithSourceCode(string fileName, string sourceCode) { @@ -213,10 +212,7 @@ private void ParseSourceCode(string sourceCode) char Next() { - if (i + 1 < sourceCode.Length) - return sourceCode[i + 1]; - - return default; + return i + 1 < sourceCode.Length ? sourceCode[i + 1] : default; } } @@ -282,10 +278,8 @@ public ProjectBuilder WithAnalyzer(DiagnosticAnalyzer diagnosticAnalyzer, string return this; } - public ProjectBuilder WithAnalyzer(string id = null, string message = null) where T : DiagnosticAnalyzer, new() - { - return WithAnalyzer(new T(), id, message); - } + public ProjectBuilder WithAnalyzer(string id = null, string message = null) where T : DiagnosticAnalyzer, new() => + WithAnalyzer(new T(), id, message); public ProjectBuilder WithCodeFixProvider(CodeFixProvider codeFixProvider) { @@ -293,10 +287,8 @@ public ProjectBuilder WithCodeFixProvider(CodeFixProvider codeFixProvider) return this; } - public ProjectBuilder WithCodeFixProvider() where T : CodeFixProvider, new() - { - return WithCodeFixProvider(new T()); - } + public ProjectBuilder WithCodeFixProvider() where T : CodeFixProvider, new() => + WithCodeFixProvider(new T()); public ProjectBuilder ShouldReportDiagnostic(params DiagnosticResult[] expectedDiagnosticResults) { @@ -315,10 +307,8 @@ public ProjectBuilder ShouldReportDiagnosticWithMessage(string message) return this; } - public ProjectBuilder ShouldFixCodeWith(string codeFix) - { - return ShouldFixCodeWith(index: null, codeFix); - } + public ProjectBuilder ShouldFixCodeWith(string codeFix) => + ShouldFixCodeWith(index: null, codeFix); public ProjectBuilder ShouldFixCodeWith(int? index, string codeFix) { @@ -327,10 +317,8 @@ public ProjectBuilder ShouldFixCodeWith(int? index, string codeFix) return this; } - public ProjectBuilder ShouldBatchFixCodeWith(string codeFix) - { - return ShouldBatchFixCodeWith(index: null, codeFix); - } + public ProjectBuilder ShouldBatchFixCodeWith(string codeFix) => + ShouldBatchFixCodeWith(index: null, codeFix); public ProjectBuilder ShouldBatchFixCodeWith(int? index, string codeFix) { diff --git a/tests/Meziantou.Analyzer.Test/Rules/FileNameMustMatchTypeNameAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/FileNameMustMatchTypeNameAnalyzerTests.cs index 624336491..0121c613a 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/FileNameMustMatchTypeNameAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/FileNameMustMatchTypeNameAnalyzerTests.cs @@ -247,6 +247,78 @@ struct Bar {} .ValidateAsync(); } + [Theory] + [InlineData("Sample")] + [InlineData("T:MyNamespace.Sample")] + public async Task MatchExcludedSymbolNames_ExactMatch(string value) + { + await CreateProjectBuilder() + .WithSourceCode(fileName: "Test0.cs", """ + namespace MyNamespace { + class Test0 {} + class Sample {} + } + """) + .AddAnalyzerConfiguration("dotnet_diagnostic.MA0048.excluded_symbol_names", value) + .ValidateAsync(); + } + + [Theory] + [InlineData("Sample1|Sample2")] + [InlineData("T:MyNamespace.Sample1|T:MyNamespace.Sample2")] + [InlineData("Sample1|T:MyNamespace.Sample2")] + public async Task MatchExcludedSymbolNames_ExactMatch_Pipe(string value) + { + await CreateProjectBuilder() + .WithSourceCode(fileName: "Test0.cs", """ + namespace MyNamespace { + class Test0 {} + class Sample1 {} + class Sample2 {} + } + """) + .AddAnalyzerConfiguration("dotnet_diagnostic.MA0048.excluded_symbol_names", value) + .ValidateAsync(); + } + + [Theory] + [InlineData("Sample*")] + [InlineData("*ample*")] + public async Task MatchExcludedSymbolNames_WildcardMatch(string value) + { + await CreateProjectBuilder() + .WithSourceCode(fileName: "Test0.cs", """ + namespace MyNamespace { + class Test0 {} + class Sample1 {} + class Sample2 {} + } + """) + .AddAnalyzerConfiguration("dotnet_diagnostic.MA0048.excluded_symbol_names", value) + .ValidateAsync(); + } + + [Theory] + [InlineData("Sample*|*1|*2")] + [InlineData("*ample*|*oo*")] + [InlineData("T:MyNamespace.Sample*|T:MyNamespace.Foo*")] + [InlineData("T:MyNamespace.Sample*|Foo*")] + public async Task MatchExcludedSymbolNames_WildcardMatch_Pipe(string value) + { + await CreateProjectBuilder() + .WithSourceCode(fileName: "Test0.cs", """ + namespace MyNamespace { + class Test0 {} + class Sample1 {} + class Sample2 {} + class Foo1 {} + class Foo2 {} + } + """) + .AddAnalyzerConfiguration("dotnet_diagnostic.MA0048.excluded_symbol_names", value) + .ValidateAsync(); + } + #if ROSLYN_4_4_OR_GREATER [Fact] public async Task FileLocalTypes()