Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 XML comment syntax|ℹ️|❌|✔️|

<!-- 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 XML comment syntax|<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 XML comment syntax
<!-- 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 XML documentation comments written on a single line when they 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 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 XML element already spans multiple lines
- The element contains multiple lines of source content
- The element contains CDATA sections
- The element contains nested XML elements (like `<c>`, `<see>`, etc.)

## 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: Multiple lines of source content
/// <summary>
/// Returns the sum of two numbers.
/// This method handles integer overflow.
/// </summary>
public int Add(int a, int b) => a + b;
````
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System.Collections.Immutable;
using System.Composition;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
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;

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

var title = "Use multi-line XML comment syntax";
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 = new StringBuilder();
foreach (var content in elementSyntax.Content)
{
if (content is not XmlTextSyntax textSyntax)
continue;

foreach (var token in textSyntax.TextTokens)
{
var text = token.Text.Trim();
if (string.IsNullOrWhiteSpace(text))
continue;

if (contentText.Length > 0)
{
contentText.Append(' ');
}

contentText.Append(text);
}
}

var startTagText = elementSyntax.StartTag.ToString();
var endTagText = elementSyntax.EndTag.ToString();
replacementText = contentText.Length == 0
? startTagText + lineBreak + commentPrefix + endTagText
: 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 XML comment syntax
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 XML comment syntax
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 XML comment syntax
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 XML comment syntax
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 XML comment syntax
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
101 changes: 101 additions & 0 deletions src/Meziantou.Analyzer/Rules/UseMultiLineXmlCommentSyntaxAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
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 XML comment syntax",
messageFormat: "Use multi-line XML comment syntax",
RuleCategories.Style,
DiagnosticSeverity.Info,
isEnabledByDefault: false,
description: "Enforce multi-line XML documentation comment syntax for consistency.",
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;

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

var hasCDataOrOtherElements = false;
var meaningfulTextTokenCount = 0;
foreach (var content in elementSyntax.Content)
{
if (content is XmlTextSyntax textSyntax)
{
foreach (var token in textSyntax.TextTokens)
{
if (token.IsKind(SyntaxKind.XmlTextLiteralNewLineToken))
continue;

var text = token.Text.Trim();
if (!string.IsNullOrWhiteSpace(text))
{
meaningfulTextTokenCount++;
}
}
}
else if (content is XmlCDataSectionSyntax || content is XmlElementSyntax)
{
hasCDataOrOtherElements = true;
break;
}
}

if (!hasCDataOrOtherElements && meaningfulTextTokenCount <= 1)
{
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