diff --git a/README.md b/README.md
index 68afabfa..4200d685 100755
--- a/README.md
+++ b/README.md
@@ -189,6 +189,8 @@ If you are already using other analyzers, you can check [which rules are duplica
|[MA0171](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0171.md)|Usage|Use pattern matching instead of inequality operators for discrete value|ℹ️|❌|✔️|
|[MA0172](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0172.md)|Usage|Both sides of the logical operation are identical|⚠️|❌|❌|
|[MA0173](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0173.md)|Design|Use LazyInitializer.EnsureInitialize|ℹ️|✔️|❌|
+|[MA0174](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0174.md)|Style|Record should use explicit 'class' keyword|ℹ️|❌|❌|
+|[MA0175](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0175.md)|Style|Record should not use explicit 'class' keyword|ℹ️|❌|❌|
diff --git a/docs/README.md b/docs/README.md
index df614d90..9732a745 100755
--- a/docs/README.md
+++ b/docs/README.md
@@ -173,6 +173,8 @@
|[MA0171](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0171.md)|Usage|Use pattern matching instead of inequality operators for discrete value|ℹ️|❌|✔️|
|[MA0172](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0172.md)|Usage|Both sides of the logical operation are identical|⚠️|❌|❌|
|[MA0173](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0173.md)|Design|Use LazyInitializer.EnsureInitialize|ℹ️|✔️|❌|
+|[MA0174](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0174.md)|Style|Record should use explicit 'class' keyword|ℹ️|❌|❌|
+|[MA0175](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0175.md)|Style|Record should not use explicit 'class' keyword|ℹ️|❌|❌|
|Id|Suppressed rule|Justification|
|--|---------------|-------------|
@@ -700,6 +702,12 @@ dotnet_diagnostic.MA0172.severity = none
# MA0173: Use LazyInitializer.EnsureInitialize
dotnet_diagnostic.MA0173.severity = suggestion
+
+# MA0174: Record should use explicit 'class' keyword
+dotnet_diagnostic.MA0174.severity = none
+
+# MA0175: Record should not use explicit 'class' keyword
+dotnet_diagnostic.MA0175.severity = none
```
# .editorconfig - all rules disabled
@@ -1220,4 +1228,10 @@ dotnet_diagnostic.MA0172.severity = none
# MA0173: Use LazyInitializer.EnsureInitialize
dotnet_diagnostic.MA0173.severity = none
+
+# MA0174: Record should use explicit 'class' keyword
+dotnet_diagnostic.MA0174.severity = none
+
+# MA0175: Record should not use explicit 'class' keyword
+dotnet_diagnostic.MA0175.severity = none
```
diff --git a/docs/Rules/MA0174.md b/docs/Rules/MA0174.md
new file mode 100644
index 00000000..b705d736
--- /dev/null
+++ b/docs/Rules/MA0174.md
@@ -0,0 +1,12 @@
+# MA0174 - Record should use explicit 'class' keyword
+
+This rule suggests adding the explicit `class` keyword to record declarations that don't specify it.
+
+```csharp
+public sealed record Customer; // non-compliant
+public sealed record class Customer; // compliant
+```
+
+## Related rules
+
+- [MA0175](MA0175.md) - Record should not use explicit 'class' keyword (opposite rule)
diff --git a/docs/Rules/MA0175.md b/docs/Rules/MA0175.md
new file mode 100644
index 00000000..a32f60c5
--- /dev/null
+++ b/docs/Rules/MA0175.md
@@ -0,0 +1,12 @@
+# MA0175 - Record should not use explicit 'class' keyword
+
+This rule suggests adding the explicit `class` keyword to record declarations that don't specify it.
+
+```csharp
+public sealed record class Customer; // non-compliant
+public sealed record Customer; // compliant
+```
+
+## Related rules
+
+- [MA0174](MA0174.md) - Record should use explicit 'class' keyword (opposite rule)
diff --git a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig
index 5cad7507..02b9eeee 100644
--- a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig
+++ b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig
@@ -517,3 +517,9 @@ dotnet_diagnostic.MA0172.severity = none
# MA0173: Use LazyInitializer.EnsureInitialize
dotnet_diagnostic.MA0173.severity = suggestion
+
+# MA0174: Record should use explicit 'class' keyword
+dotnet_diagnostic.MA0174.severity = none
+
+# MA0175: Record should not use explicit 'class' keyword
+dotnet_diagnostic.MA0175.severity = none
diff --git a/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
index 1770f021..b1856ed3 100644
--- a/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
+++ b/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
@@ -517,3 +517,9 @@ dotnet_diagnostic.MA0172.severity = none
# MA0173: Use LazyInitializer.EnsureInitialize
dotnet_diagnostic.MA0173.severity = none
+
+# MA0174: Record should use explicit 'class' keyword
+dotnet_diagnostic.MA0174.severity = none
+
+# MA0175: Record should not use explicit 'class' keyword
+dotnet_diagnostic.MA0175.severity = none
diff --git a/src/Meziantou.Analyzer/RuleIdentifiers.cs b/src/Meziantou.Analyzer/RuleIdentifiers.cs
index 56b55de8..91e44d7f 100755
--- a/src/Meziantou.Analyzer/RuleIdentifiers.cs
+++ b/src/Meziantou.Analyzer/RuleIdentifiers.cs
@@ -176,6 +176,8 @@ internal static class RuleIdentifiers
public const string UsePatternMatchingInsteadOfHasvalue = "MA0171";
public const string BothSideOfTheConditionAreIdentical = "MA0172";
public const string UseLazyInitializerEnsureInitialize = "MA0173";
+ public const string RecordClassDeclarationShouldBeExplicit = "MA0174";
+ public const string RecordClassDeclarationShouldBeImplicit = "MA0175";
public static string GetHelpUri(string identifier)
{
diff --git a/src/Meziantou.Analyzer/Rules/RecordClassDeclarationShouldBeExplicitAnalyzer.cs b/src/Meziantou.Analyzer/Rules/RecordClassDeclarationShouldBeExplicitAnalyzer.cs
new file mode 100644
index 00000000..cc529bbb
--- /dev/null
+++ b/src/Meziantou.Analyzer/Rules/RecordClassDeclarationShouldBeExplicitAnalyzer.cs
@@ -0,0 +1,48 @@
+#if CSHARP10_OR_GREATER
+using System.Collections.Immutable;
+using Meziantou.Analyzer.Internals;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Meziantou.Analyzer.Rules;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class RecordClassDeclarationShouldBeExplicitAnalyzer : DiagnosticAnalyzer
+{
+ private static readonly DiagnosticDescriptor Rule = new(
+ RuleIdentifiers.RecordClassDeclarationShouldBeExplicit,
+ title: "Record should use explicit 'class' keyword",
+ messageFormat: "Record should be declared with explicit 'class' keyword",
+ RuleCategories.Style,
+ DiagnosticSeverity.Info,
+ isEnabledByDefault: false,
+ description: "",
+ helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.RecordClassDeclarationShouldBeExplicit));
+
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.EnableConcurrentExecution();
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.RegisterSyntaxNodeAction(AnalyzeRecordDeclaration, SyntaxKind.RecordDeclaration);
+ }
+
+ private static void AnalyzeRecordDeclaration(SyntaxNodeAnalysisContext context)
+ {
+ var recordDeclaration = (RecordDeclarationSyntax)context.Node;
+
+ // Check if this is a record without the explicit 'class' keyword
+ // RecordDeclarationSyntax.ClassOrStructKeyword will be null/missing for implicit record classes
+ // and will contain 'class' or 'struct' for explicit ones
+ if (recordDeclaration.ClassOrStructKeyword.IsKind(SyntaxKind.None))
+ {
+ // This is an implicit record class (no 'class' or 'struct' keyword)
+ // Report diagnostic on the record keyword
+ context.ReportDiagnostic(Rule, recordDeclaration.Keyword);
+ }
+ }
+}
+#endif
diff --git a/src/Meziantou.Analyzer/Rules/RecordClassDeclarationShouldBeImplicitAnalyzer.cs b/src/Meziantou.Analyzer/Rules/RecordClassDeclarationShouldBeImplicitAnalyzer.cs
new file mode 100644
index 00000000..0422fc1b
--- /dev/null
+++ b/src/Meziantou.Analyzer/Rules/RecordClassDeclarationShouldBeImplicitAnalyzer.cs
@@ -0,0 +1,45 @@
+#if CSHARP10_OR_GREATER
+using System.Collections.Immutable;
+using Meziantou.Analyzer.Internals;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Meziantou.Analyzer.Rules;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class RecordClassDeclarationShouldBeImplicitAnalyzer : DiagnosticAnalyzer
+{
+ private static readonly DiagnosticDescriptor Rule = new(
+ RuleIdentifiers.RecordClassDeclarationShouldBeImplicit,
+ title: "Record should not use explicit 'class' keyword",
+ messageFormat: "Record should not be declared with explicit 'class' keyword",
+ RuleCategories.Style,
+ DiagnosticSeverity.Info,
+ isEnabledByDefault: false,
+ description: "",
+ helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.RecordClassDeclarationShouldBeImplicit));
+
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.EnableConcurrentExecution();
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.RegisterSyntaxNodeAction(AnalyzeRecordDeclaration, SyntaxKind.RecordDeclaration);
+ }
+
+ private static void AnalyzeRecordDeclaration(SyntaxNodeAnalysisContext context)
+ {
+ var recordDeclaration = (RecordDeclarationSyntax)context.Node;
+
+ // Check if this is a record with the explicit 'class' keyword
+ if (recordDeclaration.ClassOrStructKeyword.IsKind(SyntaxKind.ClassKeyword))
+ {
+ // This is an explicit record class - report diagnostic on the 'class' keyword
+ context.ReportDiagnostic(Rule, recordDeclaration.ClassOrStructKeyword);
+ }
+ }
+}
+#endif
\ No newline at end of file
diff --git a/tests/Meziantou.Analyzer.Test/Rules/RecordClassDeclarationShouldBeExplicitAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/RecordClassDeclarationShouldBeExplicitAnalyzerTests.cs
new file mode 100644
index 00000000..f2f980d1
--- /dev/null
+++ b/tests/Meziantou.Analyzer.Test/Rules/RecordClassDeclarationShouldBeExplicitAnalyzerTests.cs
@@ -0,0 +1,154 @@
+#if CSHARP10_OR_GREATER
+using Meziantou.Analyzer.Rules;
+using Meziantou.Analyzer.Test.Helpers;
+using TestHelper;
+
+namespace Meziantou.Analyzer.Test.Rules;
+
+public sealed class RecordClassDeclarationShouldBeExplicitAnalyzerTests
+{
+ private static ProjectBuilder CreateProjectBuilder()
+ {
+ return new ProjectBuilder()
+ .WithAnalyzer()
+ .WithTargetFramework(TargetFramework.NetLatest);
+ }
+
+ [Fact]
+ public async Task ImplicitRecordClass_ShouldReportDiagnostic()
+ {
+
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ public [|record|] Target { }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task ImplicitRecordClass_WithModifiers_ShouldReportDiagnostic()
+ {
+
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ public sealed [|record|] Target { }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task ExplicitRecordClass_ShouldNotReportDiagnostic()
+ {
+
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ public record class Target { }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task ExplicitRecordStruct_ShouldNotReportDiagnostic()
+ {
+
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ public record struct Target { }
+ """)
+ .ValidateAsync();
+ }
+
+[Fact]
+ public async Task RegularClass_ShouldNotReportDiagnostic()
+ {
+
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ public class Target { }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task RegularStruct_ShouldNotReportDiagnostic()
+ {
+
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ public struct Target { }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task ImplicitRecordClass_WithParameters_ShouldReportDiagnostic()
+ {
+
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ public [|record|] Target(int Id) { }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task ExplicitRecordClass_WithParameters_ShouldNotReportDiagnostic()
+ {
+
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ public record class Target(int Id) { }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task ImplicitRecordClass_Generic_ShouldReportDiagnostic()
+ {
+
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ public [|record|] Target { }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task ImplicitRecordClass_InNamespace_ShouldReportDiagnostic()
+ {
+
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ namespace MyNamespace
+ {
+ public [|record|] Target { }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task ImplicitRecordClass_WithInheritance_ShouldReportDiagnostic()
+ {
+
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ public abstract [|record|] BaseRecord { }
+ public [|record|] Target : BaseRecord { }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task RxplicitRecordClass_WithInheritance_ShouldNotReportDiagnostic()
+ {
+
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ public abstract record class BaseRecord { }
+ public record class Target : BaseRecord { }
+ """)
+ .ValidateAsync();
+ }
+}
+#endif
\ No newline at end of file
diff --git a/tests/Meziantou.Analyzer.Test/Rules/RecordClassDeclarationShouldBeImplicitAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/RecordClassDeclarationShouldBeImplicitAnalyzerTests.cs
new file mode 100644
index 00000000..c5a4b44e
--- /dev/null
+++ b/tests/Meziantou.Analyzer.Test/Rules/RecordClassDeclarationShouldBeImplicitAnalyzerTests.cs
@@ -0,0 +1,142 @@
+#if CSHARP10_OR_GREATER
+using Meziantou.Analyzer.Rules;
+using Meziantou.Analyzer.Test.Helpers;
+using TestHelper;
+
+namespace Meziantou.Analyzer.Test.Rules;
+
+public sealed class RecordClassDeclarationShouldBeImplicitAnalyzerTests
+{
+ private static ProjectBuilder CreateProjectBuilder()
+ {
+ return new ProjectBuilder()
+ .WithAnalyzer()
+ .WithTargetFramework(TargetFramework.NetLatest);
+ }
+
+ [Fact]
+ public async Task ExplicitRecordClass_ShouldReportDiagnostic()
+ {
+
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ public record [|class|] Target { }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task ExplicitRecordClass_WithModifiers_ShouldReportDiagnostic()
+ {
+
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ public sealed record [|class|] Target { }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task ImplicitRecordClass_ShouldNotReportDiagnostic()
+ {
+
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ public record Target { }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task ExplicitRecordStruct_ShouldNotReportDiagnostic()
+ {
+
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ public record struct Target { }
+ """)
+ .ValidateAsync();
+ }
+
+[Fact]
+ public async Task RegularClass_ShouldNotReportDiagnostic()
+ {
+
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ public class Target { }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task RegularStruct_ShouldNotReportDiagnostic()
+ {
+
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ public struct Target { }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task ExplicitRecordClass_WithParameters_ShouldReportDiagnostic()
+ {
+
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ public record [|class|] Target(int Id) { }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task ImplicitRecordClass_WithParameters_ShouldNotReportDiagnostic()
+ {
+
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ public record Target(int Id) { }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task ExplicitRecordClass_Generic_ShouldReportDiagnostic()
+ {
+
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ public record [|class|] Target { }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task ExplicitRecordClass_InNamespace_ShouldReportDiagnostic()
+ {
+
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ namespace MyNamespace
+ {
+ public record [|class|] Target { }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task ExplicitRecordClass_WithInheritance_ShouldReportDiagnostic()
+ {
+
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ public abstract record BaseRecord { }
+ public record [|class|] Target : BaseRecord { }
+ """)
+ .ValidateAsync();
+ }
+}
+#endif
\ No newline at end of file