Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ If you are already using other analyzers, you can check [which rules are duplica
|[MA0208](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0208.md)|Usage|\[FixedAddressValueType\] fields must be value types|⚠️|✔️|❌|
|[MA0209](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0209.md)|Performance|Use in keyword for in parameter|ℹ️|❌|✔️|
|[MA0210](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0210.md)|Performance|Use in keyword to call the in overload|ℹ️|❌|✔️|
|[MA0211](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0211.md)|Style|Use multi-line syntax for XML summary comments|ℹ️|❌|✔️|

<!-- rules -->

Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@
|[MA0208](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0208.md)|Usage|\[FixedAddressValueType\] fields must be value types|<span title='Warning'>⚠️</span>|✔️|❌|
|[MA0209](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0209.md)|Performance|Use in keyword for in parameter|<span title='Info'>ℹ️</span>|❌|✔️|
|[MA0210](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0210.md)|Performance|Use in keyword to call the in overload|<span title='Info'>ℹ️</span>|❌|✔️|
|[MA0211](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0211.md)|Style|Use multi-line syntax for XML summary comments|<span title='Info'>ℹ️</span>|❌|✔️|

|Id|Suppressed rule|Justification|
|--|---------------|-------------|
Expand Down
51 changes: 51 additions & 0 deletions docs/Rules/MA0211.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# MA0211 - Use multi-line syntax for XML summary comments
<!-- sources -->
Sources: [UseMultiLineXmlCommentSyntaxAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/UseMultiLineXmlCommentSyntaxAnalyzer.cs), [UseMultiLineXmlCommentSyntaxFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/UseMultiLineXmlCommentSyntaxFixer.cs)
<!-- sources -->

This rule reports single-line `<summary>` XML documentation comments when they contain non-empty content and can be expanded to the standard multi-line XML comment form.

````csharp
/// <summary>Description</summary>
public class Sample { }

// Should be
/// <summary>
/// Description
/// </summary>
public class Sample { }
````

## When to use multi-line format

Use this rule when you want `<summary>` XML documentation comments to consistently use the multi-line form, even when the content fits on a single line.

## When NOT to use multi-line format

The analyzer will NOT suggest converting when:

- The `<summary>` element already spans multiple lines
- The `<summary>` element is empty or contains only whitespace
- The XML documentation element is not a `<summary>` element

## Examples

````csharp
// Non-compliant: Single-line summary
/// <summary>Returns the sum of two numbers</summary>
public int Add(int a, int b) => a + b;

// Compliant: Multi-line summary
/// <summary>
/// Returns the sum of two numbers
/// </summary>
public int Add(int a, int b) => a + b;

// Compliant: Empty summary
/// <summary></summary>
public int Add(int a, int b) => a + b;

// Compliant: Other XML documentation elements are ignored
/// <param name="value">The value</param>
public void SetValue(int value) { }
````
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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.Text;

namespace Meziantou.Analyzer.Rules;

[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
public sealed class UseMultiLineXmlCommentSyntaxFixer : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.UseMultiLineXmlCommentSyntax);

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 (nodeToFix is not XmlElementSyntax elementSyntax)
return;

if (!string.Equals(elementSyntax.StartTag.Name.LocalName.ValueText, "summary", StringComparison.Ordinal))
return;

var sourceText = await context.Document.GetTextAsync(context.CancellationToken).ConfigureAwait(false);
if (!TryCreateReplacementText(sourceText, elementSyntax, out var replacementText))
return;

var title = "Use multi-line syntax for XML summary comments";
var codeAction = CodeAction.Create(
title,
cancellationToken => FixAsync(context.Document, elementSyntax.Span, replacementText, cancellationToken),
equivalenceKey: title);

context.RegisterCodeFix(codeAction, context.Diagnostics);
}

private static bool TryCreateReplacementText(SourceText sourceText, XmlElementSyntax elementSyntax, out string replacementText)
{
replacementText = string.Empty;

var lineSpan = elementSyntax.GetLocation().GetLineSpan();
if (lineSpan.StartLinePosition.Line != lineSpan.EndLinePosition.Line)
return false;

var line = sourceText.Lines.GetLineFromPosition(elementSyntax.SpanStart);
var commentPrefix = sourceText.ToString(TextSpan.FromBounds(line.Start, elementSyntax.SpanStart));
var lineBreak = sourceText.ToString(TextSpan.FromBounds(line.End, line.EndIncludingLineBreak));
if (lineBreak.Length == 0)
{
lineBreak = "\n";
}

var contentText = string.Concat(elementSyntax.Content.Select(static content => content.ToFullString())).Trim();
if (contentText.Length == 0)
return false;

var startTagText = elementSyntax.StartTag.ToString();
var endTagText = elementSyntax.EndTag.ToString();
replacementText = startTagText + lineBreak + commentPrefix + contentText + lineBreak + commentPrefix + endTagText;
return true;
}

private static async Task<Document> FixAsync(Document document, TextSpan span, string replacementText, CancellationToken cancellationToken)
{
var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
return document.WithText(sourceText.Replace(span, replacementText));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -625,3 +625,6 @@ dotnet_diagnostic.MA0209.severity = error

# MA0210: Use in keyword to call the in overload
dotnet_diagnostic.MA0210.severity = error

# MA0211: Use multi-line syntax for XML summary comments
dotnet_diagnostic.MA0211.severity = error
Original file line number Diff line number Diff line change
Expand Up @@ -625,3 +625,6 @@ dotnet_diagnostic.MA0209.severity = suggestion

# MA0210: Use in keyword to call the in overload
dotnet_diagnostic.MA0210.severity = suggestion

# MA0211: Use multi-line syntax for XML summary comments
dotnet_diagnostic.MA0211.severity = suggestion
Original file line number Diff line number Diff line change
Expand Up @@ -625,3 +625,6 @@ dotnet_diagnostic.MA0209.severity = warning

# MA0210: Use in keyword to call the in overload
dotnet_diagnostic.MA0210.severity = warning

# MA0211: Use multi-line syntax for XML summary comments
dotnet_diagnostic.MA0211.severity = warning
Original file line number Diff line number Diff line change
Expand Up @@ -625,3 +625,6 @@ dotnet_diagnostic.MA0209.severity = none

# MA0210: Use in keyword to call the in overload
dotnet_diagnostic.MA0210.severity = none

# MA0211: Use multi-line syntax for XML summary comments
dotnet_diagnostic.MA0211.severity = none
3 changes: 3 additions & 0 deletions src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -625,3 +625,6 @@ dotnet_diagnostic.MA0209.severity = none

# MA0210: Use in keyword to call the in overload
dotnet_diagnostic.MA0210.severity = none

# MA0211: Use multi-line syntax for XML summary comments
dotnet_diagnostic.MA0211.severity = none
1 change: 1 addition & 0 deletions src/Meziantou.Analyzer/RuleIdentifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ internal static class RuleIdentifiers
public const string FixedAddressValueTypeAttribute_FieldTypeMustBeValueType = "MA0208";
public const string UseInKeywordForInParameter = "MA0209";
public const string UseInKeywordToSelectInOverload = "MA0210";
public const string UseMultiLineXmlCommentSyntax = "MA0211";

public static string GetHelpUri(string identifier)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Meziantou.Analyzer.Rules;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class UseMultiLineXmlCommentSyntaxAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule = new(
RuleIdentifiers.UseMultiLineXmlCommentSyntax,
title: "Use multi-line syntax for XML summary comments",
messageFormat: "Use multi-line syntax for XML summary comments",
RuleCategories.Style,
DiagnosticSeverity.Info,
isEnabledByDefault: false,
description: "Enforce multi-line XML documentation comment syntax for <summary> elements.",
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.UseMultiLineXmlCommentSyntax));

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType, SymbolKind.Method, SymbolKind.Field, SymbolKind.Event, SymbolKind.Property);
}

private static void AnalyzeSymbol(SymbolAnalysisContext context)
{
var symbol = context.Symbol;
if (symbol.IsImplicitlyDeclared)
return;

if (IsCompilerGeneratedType(symbol))
return;

foreach (var syntaxReference in symbol.DeclaringSyntaxReferences)
{
var syntax = syntaxReference.GetSyntax(context.CancellationToken);
if (!syntax.HasStructuredTrivia)
continue;

foreach (var trivia in syntax.GetLeadingTrivia())
{
var structure = trivia.GetStructure();
if (structure is not DocumentationCommentTriviaSyntax documentation)
continue;

foreach (var childNode in documentation.ChildNodes())
{
if (childNode is not XmlElementSyntax elementSyntax)
continue;

if (!string.Equals(elementSyntax.StartTag.Name.LocalName.ValueText, "summary", StringComparison.Ordinal))
continue;

var startLine = elementSyntax.StartTag.GetLocation().GetLineSpan().StartLinePosition.Line;
var endLine = elementSyntax.EndTag.GetLocation().GetLineSpan().EndLinePosition.Line;
if (endLine != startLine)
continue;

if (!string.IsNullOrWhiteSpace(string.Concat(elementSyntax.Content.Select(static content => content.ToFullString()))))
context.ReportDiagnostic(Diagnostic.Create(Rule, elementSyntax.GetLocation()));
}
}
}
}

private static bool IsCompilerGeneratedType(ISymbol symbol)
{
return symbol is INamedTypeSymbol namedTypeSymbol && (namedTypeSymbol.IsImplicitClass || symbol.Name.Contains('$', StringComparison.Ordinal));
}
}
Loading