diff --git a/README.md b/README.md
index 2da19c83..c6c2de17 100755
--- a/README.md
+++ b/README.md
@@ -68,7 +68,7 @@ If you are already using other analyzers, you can check [which rules are duplica
|[MA0050](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0050.md)|Design|Validate arguments correctly in iterator methods|ℹ️|✔️|✔️|
|[MA0051](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0051.md)|Design|Method is too long|⚠️|✔️|❌|
|[MA0052](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0052.md)|Performance|Replace constant Enum.ToString with nameof|ℹ️|✔️|✔️|
-|[MA0053](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0053.md)|Design|Make class sealed|ℹ️|✔️|✔️|
+|[MA0053](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0053.md)|Design|Make class or record sealed|ℹ️|✔️|✔️|
|[MA0054](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0054.md)|Design|Embed the caught exception as innerException|⚠️|✔️|❌|
|[MA0055](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0055.md)|Design|Do not use finalizer|⚠️|✔️|❌|
|[MA0056](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0056.md)|Design|Do not call overridable members in constructor|⚠️|✔️|❌|
diff --git a/docs/README.md b/docs/README.md
index b908fc63..a44bc69d 100755
--- a/docs/README.md
+++ b/docs/README.md
@@ -52,7 +52,7 @@
|[MA0050](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0050.md)|Design|Validate arguments correctly in iterator methods|ℹ️|✔️|✔️|
|[MA0051](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0051.md)|Design|Method is too long|⚠️|✔️|❌|
|[MA0052](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0052.md)|Performance|Replace constant Enum.ToString with nameof|ℹ️|✔️|✔️|
-|[MA0053](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0053.md)|Design|Make class sealed|ℹ️|✔️|✔️|
+|[MA0053](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0053.md)|Design|Make class or record sealed|ℹ️|✔️|✔️|
|[MA0054](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0054.md)|Design|Embed the caught exception as innerException|⚠️|✔️|❌|
|[MA0055](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0055.md)|Design|Do not use finalizer|⚠️|✔️|❌|
|[MA0056](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0056.md)|Design|Do not call overridable members in constructor|⚠️|✔️|❌|
@@ -342,7 +342,7 @@ dotnet_diagnostic.MA0051.severity = warning
# MA0052: Replace constant Enum.ToString with nameof
dotnet_diagnostic.MA0052.severity = suggestion
-# MA0053: Make class sealed
+# MA0053: Make class or record sealed
dotnet_diagnostic.MA0053.severity = suggestion
# MA0054: Embed the caught exception as innerException
@@ -874,7 +874,7 @@ dotnet_diagnostic.MA0051.severity = none
# MA0052: Replace constant Enum.ToString with nameof
dotnet_diagnostic.MA0052.severity = none
-# MA0053: Make class sealed
+# MA0053: Make class or record sealed
dotnet_diagnostic.MA0053.severity = none
# MA0054: Embed the caught exception as innerException
diff --git a/docs/Rules/MA0053.md b/docs/Rules/MA0053.md
index 5a849859..16b5b776 100644
--- a/docs/Rules/MA0053.md
+++ b/docs/Rules/MA0053.md
@@ -1,9 +1,9 @@
-# MA0053 - Make class sealed
+# MA0053 - Make class or record sealed
Sources: [ClassMustBeSealedAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/ClassMustBeSealedAnalyzer.cs), [ClassMustBeSealedFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/ClassMustBeSealedFixer.cs)
-Classes should be sealed when there is no inheritor.
+Classes and records should be sealed when there is no inheritor.
- [Why Are So Many Of The Framework Classes Sealed?](https://blogs.msdn.microsoft.com/ericlippert/2004/01/22/why-are-so-many-of-the-framework-classes-sealed/)
- [Performance benefits of sealed class in .NET](https://www.meziantou.net/performance-benefits-of-sealed-class.htm)
@@ -27,15 +27,15 @@ public sealed class Bar : Foo
# Configuration
A Roslyn analyzer can only know the current project context, not the full solution.
-Therefore it cannot know if a public class is used in another project hereby making it possibly inaccurate to report this diagnostic for `public` classes.
-You can still enable this rule for `public` classes using the `.editorconfig`:
+Therefore it cannot know if a public class or record is used in another project hereby making it possibly inaccurate to report this diagnostic for `public` types.
+You can still enable this rule for `public` classes and records using the `.editorconfig`:
````
# .editorconfig file
MA0053.public_class_should_be_sealed = true
````
-Classes with `virtual` members cannot be sealed. By default, these classes are not reported. You can enable this rule for classes with `virtual` members using the `.editorconfig`:
+Classes and records with `virtual` members cannot be sealed. By default, these types are not reported. You can enable this rule for classes and records with `virtual` members using the `.editorconfig`:
````
# .editorconfig file
diff --git a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig
index 75332eb9..e7a44d2b 100644
--- a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig
+++ b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig
@@ -155,7 +155,7 @@ dotnet_diagnostic.MA0051.severity = warning
# MA0052: Replace constant Enum.ToString with nameof
dotnet_diagnostic.MA0052.severity = suggestion
-# MA0053: Make class sealed
+# MA0053: Make class or record sealed
dotnet_diagnostic.MA0053.severity = suggestion
# MA0054: Embed the caught exception as innerException
diff --git a/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
index 437df117..29afc8b8 100644
--- a/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
+++ b/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
@@ -155,7 +155,7 @@ dotnet_diagnostic.MA0051.severity = none
# MA0052: Replace constant Enum.ToString with nameof
dotnet_diagnostic.MA0052.severity = none
-# MA0053: Make class sealed
+# MA0053: Make class or record sealed
dotnet_diagnostic.MA0053.severity = none
# MA0054: Embed the caught exception as innerException
diff --git a/src/Meziantou.Analyzer/Rules/ClassMustBeSealedAnalyzer.cs b/src/Meziantou.Analyzer/Rules/ClassMustBeSealedAnalyzer.cs
index db3c9ade..944e92c8 100644
--- a/src/Meziantou.Analyzer/Rules/ClassMustBeSealedAnalyzer.cs
+++ b/src/Meziantou.Analyzer/Rules/ClassMustBeSealedAnalyzer.cs
@@ -11,8 +11,8 @@ public sealed class ClassMustBeSealedAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule = new(
RuleIdentifiers.ClassMustBeSealed,
- title: "Make class sealed",
- messageFormat: "Make class sealed",
+ title: "Make class or record sealed",
+ messageFormat: "Make class or record sealed",
RuleCategories.Design,
DiagnosticSeverity.Info,
isEnabledByDefault: true,
@@ -107,14 +107,13 @@ private bool IsPotentialSealed(AnalyzerOptions options, INamedTypeSymbol symbol,
if (symbol.IsTopLevelStatement(cancellationToken))
return false;
- if (symbol.GetMembers().Any(member => member.IsVirtual) && !SealedClassWithVirtualMember(options, symbol))
+ if (symbol.GetMembers().Any(member => member.IsVirtual && member.CanBeReferencedByName && !member.IsImplicitlyDeclared) && !SealedClassWithVirtualMember(options, symbol))
return false;
var canBeInheritedOutsideOfAssembly = symbol.IsVisibleOutsideOfAssembly() && symbol.GetMembers().OfType().Any(member => member.MethodKind is MethodKind.Constructor && member.IsVisibleOutsideOfAssembly());
if (canBeInheritedOutsideOfAssembly && !PublicClassShouldBeSealed(options, symbol))
return false;
-
return true;
}
diff --git a/tests/Meziantou.Analyzer.Test/Rules/ClassMustBeSealedAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/ClassMustBeSealedAnalyzerTests.cs
index bbb0f1bf..96335e67 100755
--- a/tests/Meziantou.Analyzer.Test/Rules/ClassMustBeSealedAnalyzerTests.cs
+++ b/tests/Meziantou.Analyzer.Test/Rules/ClassMustBeSealedAnalyzerTests.cs
@@ -254,6 +254,68 @@ internal sealed record Sample();
.ValidateAsync();
}
+ [Fact]
+ public async Task Record_Inherited_Diagnostic()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ record Base();
+
+ record [||]Derived() : Base();
+ """)
+ .ShouldFixCodeWith("""
+ record Base();
+
+ sealed record Derived() : Base();
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task Record_ImplementInterface_Diagnostic()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ interface ITest
+ {
+ }
+
+ record [||]Test() : ITest;
+ """)
+ .ShouldFixCodeWith("""
+ interface ITest
+ {
+ }
+
+ sealed record Test() : ITest;
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task Record_Public_NotReported()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ public record Sample();
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task Record_Public_WithEditorConfig_Diagnostic()
+ {
+ await CreateProjectBuilder()
+ .AddAnalyzerConfiguration("MA0053.public_class_should_be_sealed", "true")
+ .WithSourceCode("""
+ public record [||]Sample();
+ """)
+ .ShouldFixCodeWith("""
+ public sealed record Sample();
+ """)
+ .ValidateAsync();
+ }
+
[Theory]
[InlineData("private")]
[InlineData("internal")]