Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/Rules/MA0048.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ The name of the class must match to name of the file. This rule has two main rea
- Ensuring the type name is the same as the file name. This prevents renaming a type without renaming the file.
- When you navigate in the code without an IDE, such as GitHub, GitLab or most web interfaces, you can quickly find the file that you are insterested in.

The diagnostic message includes the type kind and name to provide clear context:
- `File name must match type name (class MyClass)`
- `File name must match type name (enum MyEnum)`
- `File name must match type name (interface IMyInterface)`
- `File name must match type name (struct MyStruct)`
- `File name must match type name (record MyRecord)`
- `File name must match type name (record struct MyRecordStruct)`
- `File name must match type name (delegate MyDelegate)`

````csharp
// filename: Bar.cs
class Foo // non compliant
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public sealed class FileNameMustMatchTypeNameAnalyzer : DiagnosticAnalyzer
private static readonly DiagnosticDescriptor Rule = new(
RuleIdentifiers.FileNameMustMatchTypeName,
title: "File name must match type name",
messageFormat: "File name must match type name",
messageFormat: "File name must match type name ({0} {1})",
RuleCategories.Design,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
Expand Down Expand Up @@ -124,7 +124,7 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context)
continue;
}

context.ReportDiagnostic(Rule, location);
context.ReportDiagnostic(Rule, location, GetTypeKindDisplayString(symbol), symbolName);
}
}

Expand Down Expand Up @@ -154,4 +154,25 @@ 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));
}

private static string GetTypeKindDisplayString(INamedTypeSymbol symbol)
{
return symbol.TypeKind switch
{
#if CSHARP10_OR_GREATER
TypeKind.Class when symbol.IsRecord => "record",
#endif
TypeKind.Class => "class",
#if CSHARP10_OR_GREATER
TypeKind.Struct when symbol.IsRecord => "record struct",
#endif
TypeKind.Struct => "struct",
TypeKind.Interface => "interface",
TypeKind.Enum => "enum",
TypeKind.Delegate => "delegate",
#pragma warning disable CA1308 // Normalize strings to uppercase
_ => symbol.TypeKind.ToString().ToLowerInvariant(),
#pragma warning restore CA1308 // Normalize strings to uppercase
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ await CreateProjectBuilder()
class [||]Sample
{
}")
.ShouldReportDiagnosticWithMessage("File name must match type name (class Sample)")
.ValidateAsync();
}

Expand Down Expand Up @@ -380,6 +381,89 @@ file class [||]Sample
}
")
.AddAnalyzerConfiguration("MA0048.exclude_file_local_types", "false")
.ShouldReportDiagnosticWithMessage("File name must match type name (class Sample)")
.ValidateAsync();
}

[Fact]
public async Task TypeKindIncludedInMessage_Class()
{
await CreateProjectBuilder()
.WithSourceCode(fileName: "Test.cs", @"
class [||]Sample
{
}")
.ShouldReportDiagnosticWithMessage("File name must match type name (class Sample)")
.ValidateAsync();
}

[Fact]
public async Task TypeKindIncludedInMessage_Struct()
{
await CreateProjectBuilder()
.WithSourceCode(fileName: "Test.cs", @"
struct [||]Sample
{
}")
.ShouldReportDiagnosticWithMessage("File name must match type name (struct Sample)")
.ValidateAsync();
}

[Fact]
public async Task TypeKindIncludedInMessage_Interface()
{
await CreateProjectBuilder()
.WithSourceCode(fileName: "Test.cs", @"
interface [||]ISample
{
}")
.ShouldReportDiagnosticWithMessage("File name must match type name (interface ISample)")
.ValidateAsync();
}

[Fact]
public async Task TypeKindIncludedInMessage_Enum()
{
await CreateProjectBuilder()
.WithSourceCode(fileName: "Test.cs", @"
enum [||]Sample
{
Value1
}")
.ShouldReportDiagnosticWithMessage("File name must match type name (enum Sample)")
.ValidateAsync();
}

[Fact]
public async Task TypeKindIncludedInMessage_Record()
{
await CreateProjectBuilder()
.WithSourceCode(fileName: "Test.cs", @"
record [||]Sample;")
.ShouldReportDiagnosticWithMessage("File name must match type name (record Sample)")
.ValidateAsync();
}

#if CSHARP11_OR_GREATER
[Fact]
public async Task TypeKindIncludedInMessage_RecordStruct()
{
await CreateProjectBuilder()
.WithSourceCode(fileName: "Test.cs", @"
record struct [||]Sample;")
.WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp11)
.ShouldReportDiagnosticWithMessage("File name must match type name (record struct Sample)")
.ValidateAsync();
}
#endif

[Fact]
public async Task TypeKindIncludedInMessage_Delegate()
{
await CreateProjectBuilder()
.WithSourceCode(fileName: "Test.cs", @"
delegate void [||]Sample();")
.ShouldReportDiagnosticWithMessage("File name must match type name (delegate Sample)")
.ValidateAsync();
}
#endif
Expand Down
Loading