From 5dbf3b79fe57e218146ccc5e1687d9bd2fc1da39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Fri, 8 May 2026 15:11:29 -0400 Subject: [PATCH 1/3] Split type inheritdoc diagnostics Keep MA0197 as a suggestion for dedicated type documentation. Add MA0198/MA0199 warning diagnostics for ambiguous and missing inheritdoc sources on types. Add MA0198 code fixes to suggest cref from declared interfaces, and update tests and generated documentation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 4 +- docs/README.md | 22 +- docs/Rules/MA0197.md | 29 +-- docs/Rules/MA0198.md | 35 +++ docs/Rules/MA0199.md | 28 +++ .../InheritdocShouldNotBeUsedOnTypesFixer.cs | 148 +++++++++++++ .../configuration/all-errors.editorconfig | 8 +- .../all-suggestions.editorconfig | 8 +- .../configuration/all-warnings.editorconfig | 8 +- .../configuration/default.editorconfig | 10 +- .../configuration/none.editorconfig | 8 +- src/Meziantou.Analyzer/RuleIdentifiers.cs | 2 + ...nheritdocShouldNotBeUsedOnTypesAnalyzer.cs | 60 ++++- ...tdocShouldNotBeUsedOnTypesAnalyzerTests.cs | 205 +++++++++++++++++- 14 files changed, 537 insertions(+), 38 deletions(-) create mode 100644 docs/Rules/MA0198.md create mode 100644 docs/Rules/MA0199.md create mode 100644 src/Meziantou.Analyzer.CodeFixers/Rules/InheritdocShouldNotBeUsedOnTypesFixer.cs diff --git a/README.md b/README.md index c0ff41ea4..bd4abc631 100755 --- a/README.md +++ b/README.md @@ -215,7 +215,9 @@ If you are already using other analyzers, you can check [which rules are duplica |[MA0194](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0194.md)|Usage|Merge is expressions on the same value|ℹ️|✔️|✔️| |[MA0195](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0195.md)|Usage|Do not use static fields before they are initialized|⚠️|✔️|❌| |[MA0196](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0196.md)|Design|Do not use inheritdoc on non-inheriting members|⚠️|✔️|❌| -|[MA0197](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0197.md)|Design|Do not use inheritdoc on types|⚠️|✔️|❌| +|[MA0197](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0197.md)|Design|Add dedicated documentation on types|ℹ️|✔️|❌| +|[MA0198](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0198.md)|Design|Specify cref for ambiguous inheritdoc on types|⚠️|✔️|✔️| +|[MA0199](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0199.md)|Design|Do not use inheritdoc on types without inheritance source|⚠️|✔️|❌| diff --git a/docs/README.md b/docs/README.md index 8e9a6eb1d..704af3cac 100755 --- a/docs/README.md +++ b/docs/README.md @@ -195,7 +195,9 @@ |[MA0194](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0194.md)|Usage|Merge is expressions on the same value|ℹ️|✔️|✔️| |[MA0195](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0195.md)|Usage|Do not use static fields before they are initialized|⚠️|✔️|❌| |[MA0196](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0196.md)|Design|Do not use inheritdoc on non-inheriting members|⚠️|✔️|❌| -|[MA0197](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0197.md)|Design|Do not use inheritdoc on types|⚠️|✔️|❌| +|[MA0197](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0197.md)|Design|Add dedicated documentation on types|ℹ️|✔️|❌| +|[MA0198](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0198.md)|Design|Specify cref for ambiguous inheritdoc on types|⚠️|✔️|✔️| +|[MA0199](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0199.md)|Design|Do not use inheritdoc on types without inheritance source|⚠️|✔️|❌| |Id|Suppressed rule|Justification| |--|---------------|-------------| @@ -797,8 +799,14 @@ dotnet_diagnostic.MA0195.severity = warning # MA0196: Do not use inheritdoc on non-inheriting members dotnet_diagnostic.MA0196.severity = warning -# MA0197: Do not use inheritdoc on types -dotnet_diagnostic.MA0197.severity = warning +# MA0197: Add dedicated documentation on types +dotnet_diagnostic.MA0197.severity = suggestion + +# MA0198: Specify cref for ambiguous inheritdoc on types +dotnet_diagnostic.MA0198.severity = warning + +# MA0199: Do not use inheritdoc on types without inheritance source +dotnet_diagnostic.MA0199.severity = warning ``` # .editorconfig - all rules disabled @@ -1386,6 +1394,12 @@ dotnet_diagnostic.MA0195.severity = none # MA0196: Do not use inheritdoc on non-inheriting members dotnet_diagnostic.MA0196.severity = none -# MA0197: Do not use inheritdoc on types +# MA0197: Add dedicated documentation on types dotnet_diagnostic.MA0197.severity = none + +# MA0198: Specify cref for ambiguous inheritdoc on types +dotnet_diagnostic.MA0198.severity = none + +# MA0199: Do not use inheritdoc on types without inheritance source +dotnet_diagnostic.MA0199.severity = none ``` diff --git a/docs/Rules/MA0197.md b/docs/Rules/MA0197.md index f49bed212..70d64f243 100644 --- a/docs/Rules/MA0197.md +++ b/docs/Rules/MA0197.md @@ -1,36 +1,37 @@ -# MA0197 - Do not use inheritdoc on types +# MA0197 - Add dedicated documentation on types Source: [InheritdocShouldNotBeUsedOnTypesAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/InheritdocShouldNotBeUsedOnTypesAnalyzer.cs) -Use `` on members only. -Types usually represent dedicated concepts, so inheriting the full documentation of another type is often a design smell. +Types should have dedicated documentation that describes their purpose directly. +Using `` on a type tends to copy intent from another type instead of documenting the type itself. -This rule reports `` on type declarations (`class`, `struct`, `interface`, `record`) unless `cref` is specified. +This rule reports `` on type declarations (`class`, `struct`, `interface`, `record`) when no `cref` is specified and the inheritdoc source is unambiguous. + +For ambiguous or missing inheritdoc sources, see: +- [MA0198](MA0198.md) +- [MA0199](MA0199.md) ````csharp // Non-compliant +class BaseType +{ +} + /// -class Sample +class Sample : BaseType { } ```` ````csharp // Compliant -/// -class DerivedType +/// Represents a dedicated concept for this type. +class Sample : BaseType { } class BaseType { - public virtual void M() { } -} - -class Sample : BaseType -{ - /// - public override void M() { } } ```` diff --git a/docs/Rules/MA0198.md b/docs/Rules/MA0198.md new file mode 100644 index 000000000..d8d307163 --- /dev/null +++ b/docs/Rules/MA0198.md @@ -0,0 +1,35 @@ +# MA0198 - Specify cref for ambiguous inheritdoc on types + +Sources: [InheritdocShouldNotBeUsedOnTypesAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/InheritdocShouldNotBeUsedOnTypesAnalyzer.cs), [InheritdocShouldNotBeUsedOnTypesFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/InheritdocShouldNotBeUsedOnTypesFixer.cs) + + +When a type has no base type (other than `System.Object` or `System.ValueType`) and declares multiple interfaces, `` is ambiguous without `cref`. + +This rule reports `` on type declarations (`class`, `struct`, `interface`, `record`) when: +- no `cref` is specified +- there is no applicable base type +- there are multiple declared interfaces + +Only declared interfaces are considered. Inherited interfaces from those declared interfaces are ignored. + +````csharp +// Non-compliant +interface IInterface1 { } +interface IInterface2 { } + +/// +class Sample : IInterface1, IInterface2 +{ +} +```` + +````csharp +// Compliant +interface IInterface1 { } +interface IInterface2 { } + +/// +class Sample : IInterface1, IInterface2 +{ +} +```` diff --git a/docs/Rules/MA0199.md b/docs/Rules/MA0199.md new file mode 100644 index 000000000..71a3b07cb --- /dev/null +++ b/docs/Rules/MA0199.md @@ -0,0 +1,28 @@ +# MA0199 - Do not use inheritdoc on types without inheritance source + +Source: [InheritdocShouldNotBeUsedOnTypesAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/InheritdocShouldNotBeUsedOnTypesAnalyzer.cs) + + +`` requires an inheritance source. +When a type has no base type (other than `System.Object` or `System.ValueType`) and no declared interface, inheritdoc has no source unless `cref` is specified. + +This rule reports `` on type declarations (`class`, `struct`, `interface`, `record`) when: +- no `cref` is specified +- there is no applicable base type +- there are no declared interfaces + +````csharp +// Non-compliant +/// +class Sample +{ +} +```` + +````csharp +// Compliant +/// Represents a dedicated concept for this type. +class Sample +{ +} +```` diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/InheritdocShouldNotBeUsedOnTypesFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/InheritdocShouldNotBeUsedOnTypesFixer.cs new file mode 100644 index 000000000..4bc3b8ebe --- /dev/null +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/InheritdocShouldNotBeUsedOnTypesFixer.cs @@ -0,0 +1,148 @@ +using System.Collections.Immutable; +using System.Composition; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Meziantou.Analyzer.Rules; + +[ExportCodeFixProvider(LanguageNames.CSharp), Shared] +public sealed class InheritdocShouldNotBeUsedOnTypesFixer : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.InheritdocShouldNotBeAmbiguousOnTypes); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var nodeToFix = root?.FindNode(context.Span, getInnermostNodeForTie: true, findInsideTrivia: true); + if (!TryGetInheritdocNode(nodeToFix, out var inheritdocNode, out var attributes)) + return; + + if (HasCrefAttribute(attributes)) + return; + + var typeDeclaration = nodeToFix?.FirstAncestorOrSelf(); + if (typeDeclaration is null) + return; + + var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); + if (semanticModel?.GetDeclaredSymbol(typeDeclaration, context.CancellationToken) is not INamedTypeSymbol typeSymbol) + return; + + if (HasBaseType(typeSymbol) || typeSymbol.Interfaces.Length <= 1) + return; + + if (inheritdocNode is XmlEmptyElementSyntax emptyElement) + { + RegisterCodeFixes(context, typeSymbol.Interfaces, emptyElement); + return; + } + + if (inheritdocNode is XmlElementStartTagSyntax startTag) + { + RegisterCodeFixes(context, typeSymbol.Interfaces, startTag); + } + } + + private static void RegisterCodeFixes(CodeFixContext context, ImmutableArray interfaces, XmlEmptyElementSyntax inheritdocNode) + { + foreach (var interfaceSymbol in interfaces) + { + RegisterCodeFix(context, interfaceSymbol, cancellationToken => AddCrefAttribute(context.Document, inheritdocNode, interfaceSymbol, cancellationToken)); + } + } + + private static void RegisterCodeFixes(CodeFixContext context, ImmutableArray interfaces, XmlElementStartTagSyntax inheritdocNode) + { + foreach (var interfaceSymbol in interfaces) + { + RegisterCodeFix(context, interfaceSymbol, cancellationToken => AddCrefAttribute(context.Document, inheritdocNode, interfaceSymbol, cancellationToken)); + } + } + + private static void RegisterCodeFix(CodeFixContext context, INamedTypeSymbol interfaceSymbol, Func> createChangedDocument) + { + var interfaceDisplayName = interfaceSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); + var crefValue = ToCrefValue(interfaceSymbol); + var title = $"Add cref=\"{interfaceDisplayName}\""; + var equivalenceKey = $"{title}:{crefValue}"; + + context.RegisterCodeFix( + CodeAction.Create( + title, + createChangedDocument, + equivalenceKey: equivalenceKey), + context.Diagnostics); + } + + private static async Task AddCrefAttribute(Document document, XmlEmptyElementSyntax inheritdocNode, INamedTypeSymbol interfaceSymbol, CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + var newNode = inheritdocNode.WithAttributes(inheritdocNode.Attributes.Add(XmlTextAttribute("cref", ToCrefValue(interfaceSymbol)))); + editor.ReplaceNode(inheritdocNode, newNode); + return editor.GetChangedDocument(); + } + + private static async Task AddCrefAttribute(Document document, XmlElementStartTagSyntax inheritdocNode, INamedTypeSymbol interfaceSymbol, CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + var newNode = inheritdocNode.WithAttributes(inheritdocNode.Attributes.Add(XmlTextAttribute("cref", ToCrefValue(interfaceSymbol)))); + editor.ReplaceNode(inheritdocNode, newNode); + return editor.GetChangedDocument(); + } + + private static bool TryGetInheritdocNode(SyntaxNode? node, out SyntaxNode? inheritdocNode, out SyntaxList attributes) + { + if (node?.FirstAncestorOrSelf() is { } emptyElement && IsInheritdocElement(emptyElement.Name)) + { + inheritdocNode = emptyElement; + attributes = emptyElement.Attributes; + return true; + } + + if (node?.FirstAncestorOrSelf() is { } startTag && IsInheritdocElement(startTag.Name)) + { + inheritdocNode = startTag; + attributes = startTag.Attributes; + return true; + } + + inheritdocNode = null; + attributes = default; + return false; + } + + private static bool IsInheritdocElement(XmlNameSyntax name) + { + return string.Equals(name.LocalName.Text, "inheritdoc", StringComparison.OrdinalIgnoreCase); + } + + private static bool HasCrefAttribute(SyntaxList attributes) + { + foreach (var attribute in attributes) + { + if (string.Equals(attribute.Name.LocalName.Text, "cref", StringComparison.OrdinalIgnoreCase)) + return true; + } + + return false; + } + + private static bool HasBaseType(INamedTypeSymbol symbol) + { + return symbol.BaseType is { SpecialType: not (SpecialType.System_Object or SpecialType.System_ValueType) }; + } + + private static string ToCrefValue(INamedTypeSymbol symbol) + { + return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + .Replace("global::", "", StringComparison.Ordinal) + .Replace('<', '{') + .Replace('>', '}'); + } +} diff --git a/src/Meziantou.Analyzer.Pack/configuration/all-errors.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/all-errors.editorconfig index aebb4c583..16c44dfce 100644 --- a/src/Meziantou.Analyzer.Pack/configuration/all-errors.editorconfig +++ b/src/Meziantou.Analyzer.Pack/configuration/all-errors.editorconfig @@ -584,5 +584,11 @@ dotnet_diagnostic.MA0195.severity = error # MA0196: Do not use inheritdoc on non-inheriting members dotnet_diagnostic.MA0196.severity = error -# MA0197: Do not use inheritdoc on types +# MA0197: Add dedicated documentation on types dotnet_diagnostic.MA0197.severity = error + +# MA0198: Specify cref for ambiguous inheritdoc on types +dotnet_diagnostic.MA0198.severity = error + +# MA0199: Do not use inheritdoc on types without inheritance source +dotnet_diagnostic.MA0199.severity = error diff --git a/src/Meziantou.Analyzer.Pack/configuration/all-suggestions.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/all-suggestions.editorconfig index be2752216..8d80fa61a 100644 --- a/src/Meziantou.Analyzer.Pack/configuration/all-suggestions.editorconfig +++ b/src/Meziantou.Analyzer.Pack/configuration/all-suggestions.editorconfig @@ -584,5 +584,11 @@ dotnet_diagnostic.MA0195.severity = suggestion # MA0196: Do not use inheritdoc on non-inheriting members dotnet_diagnostic.MA0196.severity = suggestion -# MA0197: Do not use inheritdoc on types +# MA0197: Add dedicated documentation on types dotnet_diagnostic.MA0197.severity = suggestion + +# MA0198: Specify cref for ambiguous inheritdoc on types +dotnet_diagnostic.MA0198.severity = suggestion + +# MA0199: Do not use inheritdoc on types without inheritance source +dotnet_diagnostic.MA0199.severity = suggestion diff --git a/src/Meziantou.Analyzer.Pack/configuration/all-warnings.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/all-warnings.editorconfig index 5b1139c51..f4adef9f2 100644 --- a/src/Meziantou.Analyzer.Pack/configuration/all-warnings.editorconfig +++ b/src/Meziantou.Analyzer.Pack/configuration/all-warnings.editorconfig @@ -584,5 +584,11 @@ dotnet_diagnostic.MA0195.severity = warning # MA0196: Do not use inheritdoc on non-inheriting members dotnet_diagnostic.MA0196.severity = warning -# MA0197: Do not use inheritdoc on types +# MA0197: Add dedicated documentation on types dotnet_diagnostic.MA0197.severity = warning + +# MA0198: Specify cref for ambiguous inheritdoc on types +dotnet_diagnostic.MA0198.severity = warning + +# MA0199: Do not use inheritdoc on types without inheritance source +dotnet_diagnostic.MA0199.severity = warning diff --git a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig index bd86d76d0..5066d3d23 100644 --- a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig +++ b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig @@ -584,5 +584,11 @@ dotnet_diagnostic.MA0195.severity = warning # MA0196: Do not use inheritdoc on non-inheriting members dotnet_diagnostic.MA0196.severity = warning -# MA0197: Do not use inheritdoc on types -dotnet_diagnostic.MA0197.severity = warning +# MA0197: Add dedicated documentation on types +dotnet_diagnostic.MA0197.severity = suggestion + +# MA0198: Specify cref for ambiguous inheritdoc on types +dotnet_diagnostic.MA0198.severity = warning + +# MA0199: Do not use inheritdoc on types without inheritance source +dotnet_diagnostic.MA0199.severity = warning diff --git a/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig index 7ea7e00de..8b9142c06 100644 --- a/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig +++ b/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig @@ -584,5 +584,11 @@ dotnet_diagnostic.MA0195.severity = none # MA0196: Do not use inheritdoc on non-inheriting members dotnet_diagnostic.MA0196.severity = none -# MA0197: Do not use inheritdoc on types +# MA0197: Add dedicated documentation on types dotnet_diagnostic.MA0197.severity = none + +# MA0198: Specify cref for ambiguous inheritdoc on types +dotnet_diagnostic.MA0198.severity = none + +# MA0199: Do not use inheritdoc on types without inheritance source +dotnet_diagnostic.MA0199.severity = none diff --git a/src/Meziantou.Analyzer/RuleIdentifiers.cs b/src/Meziantou.Analyzer/RuleIdentifiers.cs index 1c59c4743..0284b19a9 100755 --- a/src/Meziantou.Analyzer/RuleIdentifiers.cs +++ b/src/Meziantou.Analyzer/RuleIdentifiers.cs @@ -197,6 +197,8 @@ internal static class RuleIdentifiers public const string DoNotUseNotYetInitializedStaticField = "MA0195"; public const string InheritdocShouldBeUsedOnInheritingMember = "MA0196"; public const string InheritdocShouldNotBeUsedOnTypes = "MA0197"; + public const string InheritdocShouldNotBeAmbiguousOnTypes = "MA0198"; + public const string InheritdocShouldHaveSourceOnTypes = "MA0199"; public static string GetHelpUri(string identifier) { diff --git a/src/Meziantou.Analyzer/Rules/InheritdocShouldNotBeUsedOnTypesAnalyzer.cs b/src/Meziantou.Analyzer/Rules/InheritdocShouldNotBeUsedOnTypesAnalyzer.cs index d340b201d..e747f9c41 100644 --- a/src/Meziantou.Analyzer/Rules/InheritdocShouldNotBeUsedOnTypesAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/InheritdocShouldNotBeUsedOnTypesAnalyzer.cs @@ -11,15 +11,35 @@ public sealed class InheritdocShouldNotBeUsedOnTypesAnalyzer : DiagnosticAnalyze { private static readonly DiagnosticDescriptor Rule = new( RuleIdentifiers.InheritdocShouldNotBeUsedOnTypes, - title: "Do not use inheritdoc on types", - messageFormat: "Do not use '' on types; use it on members only", + title: "Add dedicated documentation on types", + messageFormat: "A type should have dedicated documentation instead of ''", RuleCategories.Design, - DiagnosticSeverity.Warning, + DiagnosticSeverity.Info, isEnabledByDefault: true, description: "", helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.InheritdocShouldNotBeUsedOnTypes)); - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + private static readonly DiagnosticDescriptor AmbiguousInheritdocRule = new( + RuleIdentifiers.InheritdocShouldNotBeAmbiguousOnTypes, + title: "Specify cref for ambiguous inheritdoc on types", + messageFormat: "Specify 'cref' for '' because this type has multiple declared interfaces and no base type", + RuleCategories.Design, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "", + helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.InheritdocShouldNotBeAmbiguousOnTypes)); + + private static readonly DiagnosticDescriptor InheritdocWithoutSourceRule = new( + RuleIdentifiers.InheritdocShouldHaveSourceOnTypes, + title: "Do not use inheritdoc on types without inheritance source", + messageFormat: "Do not use '' without 'cref' when this type has no base type and no declared interfaces", + RuleCategories.Design, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "", + helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.InheritdocShouldHaveSourceOnTypes)); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule, AmbiguousInheritdocRule, InheritdocWithoutSourceRule); public override void Initialize(AnalysisContext context) { @@ -40,6 +60,9 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context) if (symbol.IsImplicitClass || symbol.Name.Contains('$', StringComparison.Ordinal)) return; + var hasBaseType = HasBaseType(symbol); + var interfaceCount = symbol.Interfaces.Length; + foreach (var syntaxReference in symbol.DeclaringSyntaxReferences) { var syntax = syntaxReference.GetSyntax(context.CancellationToken); @@ -56,7 +79,7 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context) if (!IsInheritdocElement(element.Name) || HasCrefAttribute(element.Attributes)) continue; - context.ReportDiagnostic(Rule, element); + ReportInheritdocDiagnostic(context, element, hasBaseType, interfaceCount); } foreach (var element in documentation.DescendantNodes().OfType()) @@ -64,12 +87,37 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context) if (!IsInheritdocElement(element.StartTag.Name) || HasCrefAttribute(element.StartTag.Attributes)) continue; - context.ReportDiagnostic(Rule, element.StartTag); + ReportInheritdocDiagnostic(context, element.StartTag, hasBaseType, interfaceCount); } } } } + private static void ReportInheritdocDiagnostic(SymbolAnalysisContext context, SyntaxNode syntaxNode, bool hasBaseType, int interfaceCount) + { + if (!hasBaseType) + { + if (interfaceCount == 0) + { + context.ReportDiagnostic(InheritdocWithoutSourceRule, syntaxNode); + return; + } + + if (interfaceCount > 1) + { + context.ReportDiagnostic(AmbiguousInheritdocRule, syntaxNode); + return; + } + } + + context.ReportDiagnostic(Rule, syntaxNode); + } + + private static bool HasBaseType(INamedTypeSymbol symbol) + { + return symbol.BaseType is { SpecialType: not (SpecialType.System_Object or SpecialType.System_ValueType) }; + } + private static bool IsInheritdocElement(XmlNameSyntax name) { return string.Equals(name.LocalName.Text, "inheritdoc", StringComparison.OrdinalIgnoreCase); diff --git a/tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldNotBeUsedOnTypesAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldNotBeUsedOnTypesAnalyzerTests.cs index 06de1d0a4..38fbc8d2a 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldNotBeUsedOnTypesAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldNotBeUsedOnTypesAnalyzerTests.cs @@ -13,13 +13,65 @@ private static ProjectBuilder CreateProjectBuilder() .WithTargetFramework(TargetFramework.NetLatest); } + private static ProjectBuilder CreateProjectBuilderWithCodeFixProvider() + { + return CreateProjectBuilder() + .WithCodeFixProvider(); + } + [Fact] - public async Task ReportDiagnostic_Class() + public async Task ReportDiagnostic_MA0197_WhenBaseTypeIsPresent() { await CreateProjectBuilder() .WithSourceCode(""" - /// [||] - class Sample + class BaseType + { + } + + /// {|MA0197:|} + class Sample : BaseType + { + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task ReportDiagnostic_MA0197_WhenSingleDeclaredInterfaceIsPresent() + { + await CreateProjectBuilder() + .WithSourceCode(""" + interface ITest + { + } + + /// {|MA0197:|} + class Sample : ITest + { + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task ReportDiagnostic_MA0197_WhenDeclaredInterfaceInheritsMultipleInterfaces() + { + await CreateProjectBuilder() + .WithSourceCode(""" + interface IInterface1 + { + } + + interface IInterface2 + { + } + + interface ICompositeInterface : IInterface1, IInterface2 + { + } + + /// {|MA0197:|} + class Sample : ICompositeInterface { } """) @@ -53,11 +105,45 @@ class Sample } [Fact] - public async Task ReportDiagnostic_Interface() + public async Task ReportDiagnostic_MA0198_WhenMultipleDeclaredInterfacesArePresentAndNoBaseType() { await CreateProjectBuilder() .WithSourceCode(""" - /// [||] + interface IInterface1 + { + } + + interface IInterface2 + { + } + + /// {|MA0198:|} + class Sample : IInterface1, IInterface2 + { + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task ReportDiagnostic_MA0199_WhenNoBaseTypeAndNoDeclaredInterface() + { + await CreateProjectBuilder() + .WithSourceCode(""" + /// {|MA0199:|} + class Sample + { + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task ReportDiagnostic_MA0199_WhenInterfaceHasNoBaseInterface() + { + await CreateProjectBuilder() + .WithSourceCode(""" + /// {|MA0199:|} interface ITest { } @@ -84,16 +170,121 @@ public async Task ReportDiagnostic_ForEachPartialDeclaration() { await CreateProjectBuilder() .WithSourceCode(""" - /// [||] + /// {|MA0199:|} partial class Sample { } - /// [||] + /// {|MA0199:|} partial class Sample { } """) .ValidateAsync(); } + + [Fact] + public async Task CodeFix_MA0198_EmptyElement_FirstInterface() + { + await CreateProjectBuilderWithCodeFixProvider() + .WithSourceCode(""" + interface IInterface1 + { + } + + interface IInterface2 + { + } + + /// {|MA0198:|} + class Sample : IInterface1, IInterface2 + { + } + """) + .ShouldFixCodeWith(index: 0, """ + interface IInterface1 + { + } + + interface IInterface2 + { + } + + /// + class Sample : IInterface1, IInterface2 + { + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task CodeFix_MA0198_EmptyElement_SecondInterface() + { + await CreateProjectBuilderWithCodeFixProvider() + .WithSourceCode(""" + interface IInterface1 + { + } + + interface IInterface2 + { + } + + /// {|MA0198:|} + class Sample : IInterface1, IInterface2 + { + } + """) + .ShouldFixCodeWith(index: 1, """ + interface IInterface1 + { + } + + interface IInterface2 + { + } + + /// + class Sample : IInterface1, IInterface2 + { + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task CodeFix_MA0198_XmlElement() + { + await CreateProjectBuilderWithCodeFixProvider() + .WithSourceCode(""" + interface IInterface1 + { + } + + interface IInterface2 + { + } + + /// {|MA0198:|} + class Sample : IInterface1, IInterface2 + { + } + """) + .ShouldFixCodeWith(index: 1, """ + interface IInterface1 + { + } + + interface IInterface2 + { + } + + /// + class Sample : IInterface1, IInterface2 + { + } + """) + .ValidateAsync(); + } } From 75a4f79f5f7296a41c6c16fbe4390db0f49898f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Fri, 8 May 2026 15:26:58 -0400 Subject: [PATCH 2/3] Address PR feedback on inheritdoc analyzers Split MA0197, MA0198, and MA0199 into dedicated analyzers with shared helper logic, split tests into three corresponding classes, and update MA0198 cref generation to use DocumentationCommentId for code fixes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/Rules/MA0198.md | 2 +- docs/Rules/MA0199.md | 2 +- .../InheritdocShouldNotBeUsedOnTypesFixer.cs | 5 +- .../Rules/InheritdocOnTypesAnalyzerHelper.cs | 76 ++++++++ ...heritdocShouldHaveSourceOnTypesAnalyzer.cs | 32 ++++ ...tdocShouldNotBeAmbiguousOnTypesAnalyzer.cs | 32 ++++ ...nheritdocShouldNotBeUsedOnTypesAnalyzer.cs | 112 +---------- ...docShouldHaveSourceOnTypesAnalyzerTests.cs | 59 ++++++ ...houldNotBeAmbiguousOnTypesAnalyzerTests.cs | 147 +++++++++++++++ ...tdocShouldNotBeUsedOnTypesAnalyzerTests.cs | 176 ------------------ 10 files changed, 353 insertions(+), 290 deletions(-) create mode 100644 src/Meziantou.Analyzer/Rules/InheritdocOnTypesAnalyzerHelper.cs create mode 100644 src/Meziantou.Analyzer/Rules/InheritdocShouldHaveSourceOnTypesAnalyzer.cs create mode 100644 src/Meziantou.Analyzer/Rules/InheritdocShouldNotBeAmbiguousOnTypesAnalyzer.cs create mode 100644 tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldHaveSourceOnTypesAnalyzerTests.cs create mode 100644 tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldNotBeAmbiguousOnTypesAnalyzerTests.cs diff --git a/docs/Rules/MA0198.md b/docs/Rules/MA0198.md index d8d307163..988eddfd9 100644 --- a/docs/Rules/MA0198.md +++ b/docs/Rules/MA0198.md @@ -1,6 +1,6 @@ # MA0198 - Specify cref for ambiguous inheritdoc on types -Sources: [InheritdocShouldNotBeUsedOnTypesAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/InheritdocShouldNotBeUsedOnTypesAnalyzer.cs), [InheritdocShouldNotBeUsedOnTypesFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/InheritdocShouldNotBeUsedOnTypesFixer.cs) +Sources: [InheritdocShouldNotBeAmbiguousOnTypesAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/InheritdocShouldNotBeAmbiguousOnTypesAnalyzer.cs), [InheritdocShouldNotBeUsedOnTypesFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/InheritdocShouldNotBeUsedOnTypesFixer.cs) When a type has no base type (other than `System.Object` or `System.ValueType`) and declares multiple interfaces, `` is ambiguous without `cref`. diff --git a/docs/Rules/MA0199.md b/docs/Rules/MA0199.md index 71a3b07cb..1a0631dab 100644 --- a/docs/Rules/MA0199.md +++ b/docs/Rules/MA0199.md @@ -1,6 +1,6 @@ # MA0199 - Do not use inheritdoc on types without inheritance source -Source: [InheritdocShouldNotBeUsedOnTypesAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/InheritdocShouldNotBeUsedOnTypesAnalyzer.cs) +Source: [InheritdocShouldHaveSourceOnTypesAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/InheritdocShouldHaveSourceOnTypesAnalyzer.cs) `` requires an inheritance source. diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/InheritdocShouldNotBeUsedOnTypesFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/InheritdocShouldNotBeUsedOnTypesFixer.cs index 4bc3b8ebe..5fad1eb6e 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Rules/InheritdocShouldNotBeUsedOnTypesFixer.cs +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/InheritdocShouldNotBeUsedOnTypesFixer.cs @@ -140,9 +140,6 @@ private static bool HasBaseType(INamedTypeSymbol symbol) private static string ToCrefValue(INamedTypeSymbol symbol) { - return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) - .Replace("global::", "", StringComparison.Ordinal) - .Replace('<', '{') - .Replace('>', '}'); + return DocumentationCommentId.CreateDeclarationId(symbol) ?? symbol.Name; } } diff --git a/src/Meziantou.Analyzer/Rules/InheritdocOnTypesAnalyzerHelper.cs b/src/Meziantou.Analyzer/Rules/InheritdocOnTypesAnalyzerHelper.cs new file mode 100644 index 000000000..d6dacda93 --- /dev/null +++ b/src/Meziantou.Analyzer/Rules/InheritdocOnTypesAnalyzerHelper.cs @@ -0,0 +1,76 @@ +using Meziantou.Analyzer.Internals; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Meziantou.Analyzer.Rules; + +internal static class InheritdocOnTypesAnalyzerHelper +{ + public static void Analyze(SymbolAnalysisContext context, Func shouldReportDiagnostic, DiagnosticDescriptor rule) + { + if (context.Symbol is not INamedTypeSymbol symbol) + return; + + if (symbol.IsImplicitlyDeclared || symbol.TypeKind is not (TypeKind.Class or TypeKind.Struct or TypeKind.Interface)) + return; + + if (symbol.IsImplicitClass || symbol.Name.Contains('$', StringComparison.Ordinal)) + return; + + var hasBaseType = HasBaseType(symbol); + var interfaceCount = symbol.Interfaces.Length; + if (!shouldReportDiagnostic(hasBaseType, interfaceCount)) + return; + + foreach (var syntaxReference in symbol.DeclaringSyntaxReferences) + { + var syntax = syntaxReference.GetSyntax(context.CancellationToken); + if (!syntax.HasStructuredTrivia) + continue; + + foreach (var trivia in syntax.GetLeadingTrivia()) + { + if (trivia.GetStructure() is not DocumentationCommentTriviaSyntax documentation) + continue; + + foreach (var element in documentation.DescendantNodes().OfType()) + { + if (!IsInheritdocElement(element.Name) || HasCrefAttribute(element.Attributes)) + continue; + + context.ReportDiagnostic(rule, element); + } + + foreach (var element in documentation.DescendantNodes().OfType()) + { + if (!IsInheritdocElement(element.StartTag.Name) || HasCrefAttribute(element.StartTag.Attributes)) + continue; + + context.ReportDiagnostic(rule, element.StartTag); + } + } + } + } + + private static bool HasBaseType(INamedTypeSymbol symbol) + { + return symbol.BaseType is { SpecialType: not (SpecialType.System_Object or SpecialType.System_ValueType) }; + } + + private static bool IsInheritdocElement(XmlNameSyntax name) + { + return string.Equals(name.LocalName.Text, "inheritdoc", StringComparison.OrdinalIgnoreCase); + } + + private static bool HasCrefAttribute(SyntaxList attributes) + { + foreach (var attribute in attributes) + { + if (string.Equals(attribute.Name.LocalName.Text, "cref", StringComparison.OrdinalIgnoreCase)) + return true; + } + + return false; + } +} diff --git a/src/Meziantou.Analyzer/Rules/InheritdocShouldHaveSourceOnTypesAnalyzer.cs b/src/Meziantou.Analyzer/Rules/InheritdocShouldHaveSourceOnTypesAnalyzer.cs new file mode 100644 index 000000000..7215be959 --- /dev/null +++ b/src/Meziantou.Analyzer/Rules/InheritdocShouldHaveSourceOnTypesAnalyzer.cs @@ -0,0 +1,32 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Meziantou.Analyzer.Rules; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InheritdocShouldHaveSourceOnTypesAnalyzer : DiagnosticAnalyzer +{ + private static readonly DiagnosticDescriptor Rule = new( + RuleIdentifiers.InheritdocShouldHaveSourceOnTypes, + title: "Do not use inheritdoc on types without inheritance source", + messageFormat: "Do not use '' without 'cref' when this type has no base type and no declared interfaces", + RuleCategories.Design, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "", + helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.InheritdocShouldHaveSourceOnTypes)); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterSymbolAction(static context => + { + InheritdocOnTypesAnalyzerHelper.Analyze(context, static (hasBaseType, interfaceCount) => !hasBaseType && interfaceCount == 0, Rule); + }, SymbolKind.NamedType); + } +} diff --git a/src/Meziantou.Analyzer/Rules/InheritdocShouldNotBeAmbiguousOnTypesAnalyzer.cs b/src/Meziantou.Analyzer/Rules/InheritdocShouldNotBeAmbiguousOnTypesAnalyzer.cs new file mode 100644 index 000000000..3da837636 --- /dev/null +++ b/src/Meziantou.Analyzer/Rules/InheritdocShouldNotBeAmbiguousOnTypesAnalyzer.cs @@ -0,0 +1,32 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Meziantou.Analyzer.Rules; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InheritdocShouldNotBeAmbiguousOnTypesAnalyzer : DiagnosticAnalyzer +{ + private static readonly DiagnosticDescriptor Rule = new( + RuleIdentifiers.InheritdocShouldNotBeAmbiguousOnTypes, + title: "Specify cref for ambiguous inheritdoc on types", + messageFormat: "Specify 'cref' for '' because this type has multiple declared interfaces and no base type", + RuleCategories.Design, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "", + helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.InheritdocShouldNotBeAmbiguousOnTypes)); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterSymbolAction(static context => + { + InheritdocOnTypesAnalyzerHelper.Analyze(context, static (hasBaseType, interfaceCount) => !hasBaseType && interfaceCount > 1, Rule); + }, SymbolKind.NamedType); + } +} diff --git a/src/Meziantou.Analyzer/Rules/InheritdocShouldNotBeUsedOnTypesAnalyzer.cs b/src/Meziantou.Analyzer/Rules/InheritdocShouldNotBeUsedOnTypesAnalyzer.cs index e747f9c41..6be7f4137 100644 --- a/src/Meziantou.Analyzer/Rules/InheritdocShouldNotBeUsedOnTypesAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/InheritdocShouldNotBeUsedOnTypesAnalyzer.cs @@ -1,7 +1,5 @@ using System.Collections.Immutable; -using Meziantou.Analyzer.Internals; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; namespace Meziantou.Analyzer.Rules; @@ -19,118 +17,16 @@ public sealed class InheritdocShouldNotBeUsedOnTypesAnalyzer : DiagnosticAnalyze description: "", helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.InheritdocShouldNotBeUsedOnTypes)); - private static readonly DiagnosticDescriptor AmbiguousInheritdocRule = new( - RuleIdentifiers.InheritdocShouldNotBeAmbiguousOnTypes, - title: "Specify cref for ambiguous inheritdoc on types", - messageFormat: "Specify 'cref' for '' because this type has multiple declared interfaces and no base type", - RuleCategories.Design, - DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "", - helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.InheritdocShouldNotBeAmbiguousOnTypes)); - - private static readonly DiagnosticDescriptor InheritdocWithoutSourceRule = new( - RuleIdentifiers.InheritdocShouldHaveSourceOnTypes, - title: "Do not use inheritdoc on types without inheritance source", - messageFormat: "Do not use '' without 'cref' when this type has no base type and no declared interfaces", - RuleCategories.Design, - DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "", - helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.InheritdocShouldHaveSourceOnTypes)); - - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule, AmbiguousInheritdocRule, InheritdocWithoutSourceRule); + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); public override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType); - } - - private static void AnalyzeSymbol(SymbolAnalysisContext context) - { - if (context.Symbol is not INamedTypeSymbol symbol) - return; - - if (symbol.IsImplicitlyDeclared || symbol.TypeKind is not (TypeKind.Class or TypeKind.Struct or TypeKind.Interface)) - return; - - if (symbol.IsImplicitClass || symbol.Name.Contains('$', StringComparison.Ordinal)) - return; - - var hasBaseType = HasBaseType(symbol); - var interfaceCount = symbol.Interfaces.Length; - - foreach (var syntaxReference in symbol.DeclaringSyntaxReferences) + context.RegisterSymbolAction(static context => { - var syntax = syntaxReference.GetSyntax(context.CancellationToken); - if (!syntax.HasStructuredTrivia) - continue; - - foreach (var trivia in syntax.GetLeadingTrivia()) - { - if (trivia.GetStructure() is not DocumentationCommentTriviaSyntax documentation) - continue; - - foreach (var element in documentation.DescendantNodes().OfType()) - { - if (!IsInheritdocElement(element.Name) || HasCrefAttribute(element.Attributes)) - continue; - - ReportInheritdocDiagnostic(context, element, hasBaseType, interfaceCount); - } - - foreach (var element in documentation.DescendantNodes().OfType()) - { - if (!IsInheritdocElement(element.StartTag.Name) || HasCrefAttribute(element.StartTag.Attributes)) - continue; - - ReportInheritdocDiagnostic(context, element.StartTag, hasBaseType, interfaceCount); - } - } - } - } - - private static void ReportInheritdocDiagnostic(SymbolAnalysisContext context, SyntaxNode syntaxNode, bool hasBaseType, int interfaceCount) - { - if (!hasBaseType) - { - if (interfaceCount == 0) - { - context.ReportDiagnostic(InheritdocWithoutSourceRule, syntaxNode); - return; - } - - if (interfaceCount > 1) - { - context.ReportDiagnostic(AmbiguousInheritdocRule, syntaxNode); - return; - } - } - - context.ReportDiagnostic(Rule, syntaxNode); - } - - private static bool HasBaseType(INamedTypeSymbol symbol) - { - return symbol.BaseType is { SpecialType: not (SpecialType.System_Object or SpecialType.System_ValueType) }; - } - - private static bool IsInheritdocElement(XmlNameSyntax name) - { - return string.Equals(name.LocalName.Text, "inheritdoc", StringComparison.OrdinalIgnoreCase); - } - - private static bool HasCrefAttribute(SyntaxList attributes) - { - foreach (var attribute in attributes) - { - if (string.Equals(attribute.Name.LocalName.Text, "cref", StringComparison.OrdinalIgnoreCase)) - return true; - } - - return false; + InheritdocOnTypesAnalyzerHelper.Analyze(context, static (hasBaseType, interfaceCount) => hasBaseType || interfaceCount == 1, Rule); + }, SymbolKind.NamedType); } } diff --git a/tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldHaveSourceOnTypesAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldHaveSourceOnTypesAnalyzerTests.cs new file mode 100644 index 000000000..a58fabb3a --- /dev/null +++ b/tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldHaveSourceOnTypesAnalyzerTests.cs @@ -0,0 +1,59 @@ +using Meziantou.Analyzer.Rules; +using Meziantou.Analyzer.Test.Helpers; +using TestHelper; + +namespace Meziantou.Analyzer.Test.Rules; + +public sealed class InheritdocShouldHaveSourceOnTypesAnalyzerTests +{ + private static ProjectBuilder CreateProjectBuilder() + { + return new ProjectBuilder() + .WithAnalyzer() + .WithTargetFramework(TargetFramework.NetLatest); + } + + [Fact] + public async Task ReportDiagnostic_MA0199_WhenNoBaseTypeAndNoDeclaredInterface() + { + await CreateProjectBuilder() + .WithSourceCode(""" + /// {|MA0199:|} + class Sample + { + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task ReportDiagnostic_MA0199_WhenInterfaceHasNoBaseInterface() + { + await CreateProjectBuilder() + .WithSourceCode(""" + /// {|MA0199:|} + interface ITest + { + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task ReportDiagnostic_ForEachPartialDeclaration() + { + await CreateProjectBuilder() + .WithSourceCode(""" + /// {|MA0199:|} + partial class Sample + { + } + + /// {|MA0199:|} + partial class Sample + { + } + """) + .ValidateAsync(); + } +} diff --git a/tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldNotBeAmbiguousOnTypesAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldNotBeAmbiguousOnTypesAnalyzerTests.cs new file mode 100644 index 000000000..fef9a3d36 --- /dev/null +++ b/tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldNotBeAmbiguousOnTypesAnalyzerTests.cs @@ -0,0 +1,147 @@ +using Meziantou.Analyzer.Rules; +using Meziantou.Analyzer.Test.Helpers; +using TestHelper; + +namespace Meziantou.Analyzer.Test.Rules; + +public sealed class InheritdocShouldNotBeAmbiguousOnTypesAnalyzerTests +{ + private static ProjectBuilder CreateProjectBuilder() + { + return new ProjectBuilder() + .WithAnalyzer() + .WithTargetFramework(TargetFramework.NetLatest); + } + + private static ProjectBuilder CreateProjectBuilderWithCodeFixProvider() + { + return CreateProjectBuilder() + .WithCodeFixProvider(); + } + + [Fact] + public async Task ReportDiagnostic_MA0198_WhenMultipleDeclaredInterfacesArePresentAndNoBaseType() + { + await CreateProjectBuilder() + .WithSourceCode(""" + interface IInterface1 + { + } + + interface IInterface2 + { + } + + /// {|MA0198:|} + class Sample : IInterface1, IInterface2 + { + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task CodeFix_MA0198_EmptyElement_FirstInterface() + { + await CreateProjectBuilderWithCodeFixProvider() + .WithSourceCode(""" + interface IInterface1 + { + } + + interface IInterface2 + { + } + + /// {|MA0198:|} + class Sample : IInterface1, IInterface2 + { + } + """) + .ShouldFixCodeWith(index: 0, """ + interface IInterface1 + { + } + + interface IInterface2 + { + } + + /// + class Sample : IInterface1, IInterface2 + { + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task CodeFix_MA0198_EmptyElement_SecondInterface() + { + await CreateProjectBuilderWithCodeFixProvider() + .WithSourceCode(""" + interface IInterface1 + { + } + + interface IInterface2 + { + } + + /// {|MA0198:|} + class Sample : IInterface1, IInterface2 + { + } + """) + .ShouldFixCodeWith(index: 1, """ + interface IInterface1 + { + } + + interface IInterface2 + { + } + + /// + class Sample : IInterface1, IInterface2 + { + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task CodeFix_MA0198_XmlElement() + { + await CreateProjectBuilderWithCodeFixProvider() + .WithSourceCode(""" + interface IInterface1 + { + } + + interface IInterface2 + { + } + + /// {|MA0198:|} + class Sample : IInterface1, IInterface2 + { + } + """) + .ShouldFixCodeWith(index: 1, """ + interface IInterface1 + { + } + + interface IInterface2 + { + } + + /// + class Sample : IInterface1, IInterface2 + { + } + """) + .ValidateAsync(); + } +} diff --git a/tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldNotBeUsedOnTypesAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldNotBeUsedOnTypesAnalyzerTests.cs index 38fbc8d2a..25a903618 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldNotBeUsedOnTypesAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldNotBeUsedOnTypesAnalyzerTests.cs @@ -13,12 +13,6 @@ private static ProjectBuilder CreateProjectBuilder() .WithTargetFramework(TargetFramework.NetLatest); } - private static ProjectBuilder CreateProjectBuilderWithCodeFixProvider() - { - return CreateProjectBuilder() - .WithCodeFixProvider(); - } - [Fact] public async Task ReportDiagnostic_MA0197_WhenBaseTypeIsPresent() { @@ -104,53 +98,6 @@ class Sample .ValidateAsync(); } - [Fact] - public async Task ReportDiagnostic_MA0198_WhenMultipleDeclaredInterfacesArePresentAndNoBaseType() - { - await CreateProjectBuilder() - .WithSourceCode(""" - interface IInterface1 - { - } - - interface IInterface2 - { - } - - /// {|MA0198:|} - class Sample : IInterface1, IInterface2 - { - } - """) - .ValidateAsync(); - } - - [Fact] - public async Task ReportDiagnostic_MA0199_WhenNoBaseTypeAndNoDeclaredInterface() - { - await CreateProjectBuilder() - .WithSourceCode(""" - /// {|MA0199:|} - class Sample - { - } - """) - .ValidateAsync(); - } - - [Fact] - public async Task ReportDiagnostic_MA0199_WhenInterfaceHasNoBaseInterface() - { - await CreateProjectBuilder() - .WithSourceCode(""" - /// {|MA0199:|} - interface ITest - { - } - """) - .ValidateAsync(); - } - [Fact] public async Task NoDiagnostic_WhenUsedOnMember() { @@ -164,127 +111,4 @@ class Sample """) .ValidateAsync(); } - - [Fact] - public async Task ReportDiagnostic_ForEachPartialDeclaration() - { - await CreateProjectBuilder() - .WithSourceCode(""" - /// {|MA0199:|} - partial class Sample - { - } - - /// {|MA0199:|} - partial class Sample - { - } - """) - .ValidateAsync(); - } - - [Fact] - public async Task CodeFix_MA0198_EmptyElement_FirstInterface() - { - await CreateProjectBuilderWithCodeFixProvider() - .WithSourceCode(""" - interface IInterface1 - { - } - - interface IInterface2 - { - } - - /// {|MA0198:|} - class Sample : IInterface1, IInterface2 - { - } - """) - .ShouldFixCodeWith(index: 0, """ - interface IInterface1 - { - } - - interface IInterface2 - { - } - - /// - class Sample : IInterface1, IInterface2 - { - } - """) - .ValidateAsync(); - } - - [Fact] - public async Task CodeFix_MA0198_EmptyElement_SecondInterface() - { - await CreateProjectBuilderWithCodeFixProvider() - .WithSourceCode(""" - interface IInterface1 - { - } - - interface IInterface2 - { - } - - /// {|MA0198:|} - class Sample : IInterface1, IInterface2 - { - } - """) - .ShouldFixCodeWith(index: 1, """ - interface IInterface1 - { - } - - interface IInterface2 - { - } - - /// - class Sample : IInterface1, IInterface2 - { - } - """) - .ValidateAsync(); - } - - [Fact] - public async Task CodeFix_MA0198_XmlElement() - { - await CreateProjectBuilderWithCodeFixProvider() - .WithSourceCode(""" - interface IInterface1 - { - } - - interface IInterface2 - { - } - - /// {|MA0198:|} - class Sample : IInterface1, IInterface2 - { - } - """) - .ShouldFixCodeWith(index: 1, """ - interface IInterface1 - { - } - - interface IInterface2 - { - } - - /// - class Sample : IInterface1, IInterface2 - { - } - """) - .ValidateAsync(); - } } From 7863f3da2a7337bae5a266e9470a76316e365606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Fri, 8 May 2026 16:17:02 -0400 Subject: [PATCH 3/3] Address follow-up PR comments Use DocumentationCommentId.CreateReferenceId for generated cref values, apply requested analyzer registration style updates, and add valid-case coverage for MA0198/MA0199 test classes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../InheritdocShouldNotBeUsedOnTypesFixer.cs | 2 +- ...heritdocShouldHaveSourceOnTypesAnalyzer.cs | 4 +- ...tdocShouldNotBeAmbiguousOnTypesAnalyzer.cs | 4 +- ...docShouldHaveSourceOnTypesAnalyzerTests.cs | 92 +++++++++++++++++++ ...houldNotBeAmbiguousOnTypesAnalyzerTests.cs | 69 +++++++++++++- 5 files changed, 163 insertions(+), 8 deletions(-) diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/InheritdocShouldNotBeUsedOnTypesFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/InheritdocShouldNotBeUsedOnTypesFixer.cs index 5fad1eb6e..0d7fb492f 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Rules/InheritdocShouldNotBeUsedOnTypesFixer.cs +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/InheritdocShouldNotBeUsedOnTypesFixer.cs @@ -140,6 +140,6 @@ private static bool HasBaseType(INamedTypeSymbol symbol) private static string ToCrefValue(INamedTypeSymbol symbol) { - return DocumentationCommentId.CreateDeclarationId(symbol) ?? symbol.Name; + return DocumentationCommentId.CreateReferenceId(symbol); } } diff --git a/src/Meziantou.Analyzer/Rules/InheritdocShouldHaveSourceOnTypesAnalyzer.cs b/src/Meziantou.Analyzer/Rules/InheritdocShouldHaveSourceOnTypesAnalyzer.cs index 7215be959..0b0c5465d 100644 --- a/src/Meziantou.Analyzer/Rules/InheritdocShouldHaveSourceOnTypesAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/InheritdocShouldHaveSourceOnTypesAnalyzer.cs @@ -24,9 +24,9 @@ public override void Initialize(AnalysisContext context) context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterSymbolAction(static context => + context.RegisterSymbolAction(context => { - InheritdocOnTypesAnalyzerHelper.Analyze(context, static (hasBaseType, interfaceCount) => !hasBaseType && interfaceCount == 0, Rule); + InheritdocOnTypesAnalyzerHelper.Analyze(context, (hasBaseType, interfaceCount) => !hasBaseType && interfaceCount == 0, Rule); }, SymbolKind.NamedType); } } diff --git a/src/Meziantou.Analyzer/Rules/InheritdocShouldNotBeAmbiguousOnTypesAnalyzer.cs b/src/Meziantou.Analyzer/Rules/InheritdocShouldNotBeAmbiguousOnTypesAnalyzer.cs index 3da837636..ed4db006f 100644 --- a/src/Meziantou.Analyzer/Rules/InheritdocShouldNotBeAmbiguousOnTypesAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/InheritdocShouldNotBeAmbiguousOnTypesAnalyzer.cs @@ -24,9 +24,9 @@ public override void Initialize(AnalysisContext context) context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterSymbolAction(static context => + context.RegisterSymbolAction(context => { - InheritdocOnTypesAnalyzerHelper.Analyze(context, static (hasBaseType, interfaceCount) => !hasBaseType && interfaceCount > 1, Rule); + InheritdocOnTypesAnalyzerHelper.Analyze(context, (hasBaseType, interfaceCount) => !hasBaseType && interfaceCount > 1, Rule); }, SymbolKind.NamedType); } } diff --git a/tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldHaveSourceOnTypesAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldHaveSourceOnTypesAnalyzerTests.cs index a58fabb3a..2bc3614ab 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldHaveSourceOnTypesAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldHaveSourceOnTypesAnalyzerTests.cs @@ -56,4 +56,96 @@ partial class Sample """) .ValidateAsync(); } + + [Fact] + public async Task NoDiagnostic_WhenBaseTypeIsPresent() + { + await CreateProjectBuilder() + .WithSourceCode(""" + class BaseClass + { + } + + /// + class Sample : BaseClass + { + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task NoDiagnostic_WhenCrefIsPresent() + { + await CreateProjectBuilder() + .WithSourceCode(""" + /// + class Sample + { + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task NoDiagnostic_WhenInterfaceInheritsAnotherInterface() + { + await CreateProjectBuilder() + .WithSourceCode(""" + interface IBase + { + } + + /// + interface IChild : IBase + { + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task NoDiagnostic_WhenRecordInheritsBaseRecord() + { + await CreateProjectBuilder() + .WithSourceCode(""" + record BaseRecord; + + /// + record Sample : BaseRecord; + """) + .ValidateAsync(); + } + + [Fact] + public async Task NoDiagnostic_WhenStructImplementsInterface() + { + await CreateProjectBuilder() + .WithSourceCode(""" + interface ITest + { + } + + /// + struct Sample : ITest + { + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task NoDiagnostic_WhenRecordStructImplementsInterface() + { + await CreateProjectBuilder() + .WithSourceCode(""" + interface ITest + { + } + + /// + record struct Sample : ITest; + """) + .ValidateAsync(); + } } diff --git a/tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldNotBeAmbiguousOnTypesAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldNotBeAmbiguousOnTypesAnalyzerTests.cs index fef9a3d36..ab8cb6428 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldNotBeAmbiguousOnTypesAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldNotBeAmbiguousOnTypesAnalyzerTests.cs @@ -67,7 +67,7 @@ interface IInterface2 { } - /// + /// class Sample : IInterface1, IInterface2 { } @@ -102,7 +102,7 @@ interface IInterface2 { } - /// + /// class Sample : IInterface1, IInterface2 { } @@ -137,7 +137,70 @@ interface IInterface2 { } - /// + /// + class Sample : IInterface1, IInterface2 + { + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task NoDiagnostic_WhenSingleDeclaredInterfaceIsPresent() + { + await CreateProjectBuilder() + .WithSourceCode(""" + interface IInterface1 + { + } + + /// + class Sample : IInterface1 + { + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task NoDiagnostic_WhenBaseTypeIsPresent() + { + await CreateProjectBuilder() + .WithSourceCode(""" + class BaseClass + { + } + + interface IInterface1 + { + } + + interface IInterface2 + { + } + + /// + class Sample : BaseClass, IInterface1, IInterface2 + { + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task NoDiagnostic_WhenCrefIsPresent() + { + await CreateProjectBuilder() + .WithSourceCode(""" + interface IInterface1 + { + } + + interface IInterface2 + { + } + + /// class Sample : IInterface1, IInterface2 { }