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")]