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..988eddfd9
--- /dev/null
+++ b/docs/Rules/MA0198.md
@@ -0,0 +1,35 @@
+# MA0198 - Specify cref for ambiguous inheritdoc on types
+
+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`.
+
+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..1a0631dab
--- /dev/null
+++ b/docs/Rules/MA0199.md
@@ -0,0 +1,28 @@
+# MA0199 - Do not use inheritdoc on types without inheritance source
+
+Source: [InheritdocShouldHaveSourceOnTypesAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/InheritdocShouldHaveSourceOnTypesAnalyzer.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..0d7fb492f
--- /dev/null
+++ b/src/Meziantou.Analyzer.CodeFixers/Rules/InheritdocShouldNotBeUsedOnTypesFixer.cs
@@ -0,0 +1,145 @@
+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 DocumentationCommentId.CreateReferenceId(symbol);
+ }
+}
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/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..0b0c5465d
--- /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(context =>
+ {
+ 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
new file mode 100644
index 000000000..ed4db006f
--- /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(context =>
+ {
+ InheritdocOnTypesAnalyzerHelper.Analyze(context, (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 d340b201d..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;
@@ -11,10 +9,10 @@ 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));
@@ -26,63 +24,9 @@ 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;
-
- 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;
-
- 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 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..2bc3614ab
--- /dev/null
+++ b/tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldHaveSourceOnTypesAnalyzerTests.cs
@@ -0,0 +1,151 @@
+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();
+ }
+
+ [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
new file mode 100644
index 000000000..ab8cb6428
--- /dev/null
+++ b/tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldNotBeAmbiguousOnTypesAnalyzerTests.cs
@@ -0,0 +1,210 @@
+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();
+ }
+
+ [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
+ {
+ }
+ """)
+ .ValidateAsync();
+ }
+}
diff --git a/tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldNotBeUsedOnTypesAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldNotBeUsedOnTypesAnalyzerTests.cs
index 06de1d0a4..25a903618 100644
--- a/tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldNotBeUsedOnTypesAnalyzerTests.cs
+++ b/tests/Meziantou.Analyzer.Test/Rules/InheritdocShouldNotBeUsedOnTypesAnalyzerTests.cs
@@ -14,12 +14,16 @@ private static ProjectBuilder CreateProjectBuilder()
}
[Fact]
- public async Task ReportDiagnostic_Class()
+ public async Task ReportDiagnostic_MA0197_WhenBaseTypeIsPresent()
{
await CreateProjectBuilder()
.WithSourceCode("""
- /// [||]
- class Sample
+ class BaseType
+ {
+ }
+
+ /// {|MA0197:|}
+ class Sample : BaseType
{
}
""")
@@ -27,12 +31,16 @@ class Sample
}
[Fact]
- public async Task NoDiagnostic_WhenCrefIsPresent()
+ public async Task ReportDiagnostic_MA0197_WhenSingleDeclaredInterfaceIsPresent()
{
await CreateProjectBuilder()
.WithSourceCode("""
- ///
- class Sample
+ interface ITest
+ {
+ }
+
+ /// {|MA0197:|}
+ class Sample : ITest
{
}
""")
@@ -40,12 +48,24 @@ class Sample
}
[Fact]
- public async Task NoDiagnostic_WhenCrefIsPresentOnXmlElement()
+ public async Task ReportDiagnostic_MA0197_WhenDeclaredInterfaceInheritsMultipleInterfaces()
{
await CreateProjectBuilder()
.WithSourceCode("""
- ///
- class Sample
+ interface IInterface1
+ {
+ }
+
+ interface IInterface2
+ {
+ }
+
+ interface ICompositeInterface : IInterface1, IInterface2
+ {
+ }
+
+ /// {|MA0197:|}
+ class Sample : ICompositeInterface
{
}
""")
@@ -53,12 +73,12 @@ class Sample
}
[Fact]
- public async Task ReportDiagnostic_Interface()
+ public async Task NoDiagnostic_WhenCrefIsPresent()
{
await CreateProjectBuilder()
.WithSourceCode("""
- /// [||]
- interface ITest
+ ///
+ class Sample
{
}
""")
@@ -66,32 +86,27 @@ interface ITest
}
[Fact]
- public async Task NoDiagnostic_WhenUsedOnMember()
+ public async Task NoDiagnostic_WhenCrefIsPresentOnXmlElement()
{
await CreateProjectBuilder()
.WithSourceCode("""
+ ///
class Sample
{
- ///
- public override string ToString() => base.ToString();
}
""")
.ValidateAsync();
}
[Fact]
- public async Task ReportDiagnostic_ForEachPartialDeclaration()
+ public async Task NoDiagnostic_WhenUsedOnMember()
{
await CreateProjectBuilder()
.WithSourceCode("""
- /// [||]
- partial class Sample
- {
- }
-
- /// [||]
- partial class Sample
+ class Sample
{
+ ///
+ public override string ToString() => base.ToString();
}
""")
.ValidateAsync();