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();