Skip to content

Commit

Permalink
Added support for excluded_symbol_names to FileNameMustMatchTypeNameA…
Browse files Browse the repository at this point in the history
…nalyzer (MA0048) (#611)
  • Loading branch information
abatishchev authored Oct 11, 2023
1 parent 8176292 commit cfa7ab7
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 35 deletions.
3 changes: 3 additions & 0 deletions docs/Rules/MA0048.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
````
41 changes: 38 additions & 3 deletions src/Meziantou.Analyzer/Rules/FileNameMustMatchTypeNameAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -44,14 +45,36 @@ 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
if (symbol.IsFileLocal && context.Options.GetConfigurationValue(location.SourceTree, s_rule.Id + ".exclude_file_local_types", defaultValue: true))
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);
Expand All @@ -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;

Expand Down Expand Up @@ -115,4 +138,16 @@ private static ReadOnlySpan<char> GetFileName(ReadOnlySpan<char> filePath)

return filePath[..index];
}

/// <summary>
/// Implemented wildcard pattern match
/// </summary>
/// <example>
/// Would match FooManager for expression *Manager
/// </example>
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));
}
}
52 changes: 20 additions & 32 deletions tests/Meziantou.Analyzer.Test/Helpers/ProjectBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,9 @@ public sealed partial class ProjectBuilder

private static Task<string[]> GetNuGetReferences(string packageName, string version, params string[] paths)
{
var task = s_cache.GetOrAdd(packageName + '@' + version + ':' + string.Join(",", paths), key =>
{
return new Lazy<Task<string[]>>(Download);
});
var task = s_cache.GetOrAdd(
packageName + '@' + version + ':' + string.Join(",", paths),
_ => new Lazy<Task<string[]>>(Download));

return task.Value;

Expand Down Expand Up @@ -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/");

Expand All @@ -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)
{
Expand Down Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -282,21 +278,17 @@ public ProjectBuilder WithAnalyzer(DiagnosticAnalyzer diagnosticAnalyzer, string
return this;
}

public ProjectBuilder WithAnalyzer<T>(string id = null, string message = null) where T : DiagnosticAnalyzer, new()
{
return WithAnalyzer(new T(), id, message);
}
public ProjectBuilder WithAnalyzer<T>(string id = null, string message = null) where T : DiagnosticAnalyzer, new() =>
WithAnalyzer(new T(), id, message);

public ProjectBuilder WithCodeFixProvider(CodeFixProvider codeFixProvider)
{
CodeFixProvider = codeFixProvider;
return this;
}

public ProjectBuilder WithCodeFixProvider<T>() where T : CodeFixProvider, new()
{
return WithCodeFixProvider(new T());
}
public ProjectBuilder WithCodeFixProvider<T>() where T : CodeFixProvider, new() =>
WithCodeFixProvider(new T());

public ProjectBuilder ShouldReportDiagnostic(params DiagnosticResult[] expectedDiagnosticResults)
{
Expand All @@ -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)
{
Expand All @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit cfa7ab7

Please sign in to comment.