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 @@ -192,6 +192,7 @@ If you are already using other analyzers, you can check [which rules are duplica
|[MA0174](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0174.md)|Style|Record should use explicit 'class' keyword|ℹ️|❌|❌|
|[MA0175](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0175.md)|Style|Record should not use explicit 'class' keyword|ℹ️|❌|❌|
|[MA0176](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0176.md)|Performance|Optimize guid creation|ℹ️|✔️|✔️|
|[MA0177](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0177.md)|Style|Use single-line XML comment syntax when possible|ℹ️|❌|✔️|

<!-- rules -->

Expand Down
7 changes: 7 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@
|[MA0174](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0174.md)|Style|Record should use explicit 'class' keyword|<span title='Info'>ℹ️</span>|❌|❌|
|[MA0175](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0175.md)|Style|Record should not use explicit 'class' keyword|<span title='Info'>ℹ️</span>|❌|❌|
|[MA0176](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0176.md)|Performance|Optimize guid creation|<span title='Info'>ℹ️</span>|✔️|✔️|
|[MA0177](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0177.md)|Style|Use single-line XML comment syntax when possible|<span title='Info'>ℹ️</span>|❌|✔️|

|Id|Suppressed rule|Justification|
|--|---------------|-------------|
Expand Down Expand Up @@ -712,6 +713,9 @@ dotnet_diagnostic.MA0175.severity = none

# MA0176: Optimize guid creation
dotnet_diagnostic.MA0176.severity = suggestion

# MA0177: Use single-line XML comment syntax when possible
dotnet_diagnostic.MA0177.severity = none
```

# .editorconfig - all rules disabled
Expand Down Expand Up @@ -1241,4 +1245,7 @@ dotnet_diagnostic.MA0175.severity = none

# MA0176: Optimize guid creation
dotnet_diagnostic.MA0176.severity = none

# MA0177: Use single-line XML comment syntax when possible
dotnet_diagnostic.MA0177.severity = none
```
74 changes: 74 additions & 0 deletions docs/Rules/MA0177.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# MA0177 - Use single-line XML comment syntax when possible

This rule reports XML documentation comments that span multiple lines but contain only single-line content. Such comments can be more concisely written on a single line for better readability.

````csharp
/// <summary>
/// This is a description
/// </summary>
public class Sample { }

// Should be
/// <summary>This is a description</summary>
public class Sample { }
````

## When to use single-line format

The analyzer will suggest converting to single-line format when:
- The XML element spans multiple lines
- The element contains only a single line of actual text content (ignoring whitespace)
- The resulting single-line comment would fit within the `max_line_length` configuration (if set)

## When NOT to use single-line format

The analyzer will NOT suggest converting when:
- The element already uses single-line format
- The element contains multiple lines of text content
- The element contains CDATA sections
- The element contains nested XML elements (like `<c>`, `<see>`, etc.)
- The single-line version would exceed the `max_line_length` setting

## Configuration

The analyzer respects the `max_line_length` setting from your `.editorconfig` file:

````editorconfig
[*.cs]
max_line_length = 120
````

If the single-line version of the XML comment would exceed this limit, the analyzer will not report a diagnostic.

## Examples

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

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

// Compliant: Multiple lines of actual content
/// <summary>
/// Returns the sum of two numbers.
/// This method handles integer overflow.
/// </summary>
public int Add(int a, int b) => a + b;

// Compliant: Contains nested XML element
/// <summary>
/// Returns the sum using <see cref="Add"/> method
/// </summary>
public int Calculate(int a, int b) => Add(a, b);

// Compliant: Contains CDATA section
/// <summary><![CDATA[
/// Special content with <markup>
/// ]]></summary>
public void Process() { }
````
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
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.Editing;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

namespace Meziantou.Analyzer.Rules;

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

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 title = "Use single-line XML comment syntax";
var codeAction = CodeAction.Create(
title,
cancellationToken => Fix(context.Document, elementSyntax, cancellationToken),
equivalenceKey: title);

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

private static async Task<Document> Fix(Document document, XmlElementSyntax elementSyntax, CancellationToken cancellationToken)
{
var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);

// Extract the text content
var contentText = new StringBuilder();
foreach (var content in elementSyntax.Content)
{
if (content is XmlTextSyntax textSyntax)
{
foreach (var token in textSyntax.TextTokens)
{
// Skip newline tokens
if (token.IsKind(SyntaxKind.XmlTextLiteralNewLineToken))
continue;

var text = token.Text.Trim();
if (!string.IsNullOrWhiteSpace(text))
{
if (contentText.Length > 0)
contentText.Append(' ');
contentText.Append(text);
}
}
}
}

// Create single-line syntax
var elementName = elementSyntax.StartTag.Name;
var attributes = elementSyntax.StartTag.Attributes;

var newNode = XmlElement(
XmlElementStartTag(elementName, attributes),
SingletonList<XmlNodeSyntax>(XmlText(contentText.ToString())),
XmlElementEndTag(elementName))
.WithLeadingTrivia(elementSyntax.GetLeadingTrivia())
.WithTrailingTrivia(elementSyntax.GetTrailingTrivia());

editor.ReplaceNode(elementSyntax, newNode);
return editor.GetChangedDocument();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -526,3 +526,6 @@ dotnet_diagnostic.MA0175.severity = none

# MA0176: Optimize guid creation
dotnet_diagnostic.MA0176.severity = suggestion

# MA0177: Use single-line XML comment syntax when possible
dotnet_diagnostic.MA0177.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 @@ -526,3 +526,6 @@ dotnet_diagnostic.MA0175.severity = none

# MA0176: Optimize guid creation
dotnet_diagnostic.MA0176.severity = none

# MA0177: Use single-line XML comment syntax when possible
dotnet_diagnostic.MA0177.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 @@ -177,6 +177,7 @@ internal static class RuleIdentifiers
public const string RecordClassDeclarationShouldBeExplicit = "MA0174";
public const string RecordClassDeclarationShouldBeImplicit = "MA0175";
public const string OptimizeGuidCreation = "MA0176";
public const string UseSingleLineXmlCommentSyntaxWhenPossible = "MA0177";

public static string GetHelpUri(string identifier)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
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 UseInlineXmlCommentSyntaxWhenPossibleAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule = new(
RuleIdentifiers.UseSingleLineXmlCommentSyntaxWhenPossible,
title: "Use single-line XML comment syntax when possible",
messageFormat: "Use single-line XML comment syntax when possible",
RuleCategories.Style,
DiagnosticSeverity.Info,
isEnabledByDefault: false,
description: "",
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.UseSingleLineXmlCommentSyntaxWhenPossible));

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 (symbol is INamedTypeSymbol namedTypeSymbol && (namedTypeSymbol.IsImplicitClass || symbol.Name.Contains('$', StringComparison.Ordinal)))
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 null)
continue;

if (structure is not DocumentationCommentTriviaSyntax documentation)
continue;

foreach (var childNode in documentation.ChildNodes())
{
if (childNode is XmlElementSyntax elementSyntax)
{
// Check if element spans multiple lines
var startLine = elementSyntax.StartTag.GetLocation().GetLineSpan().StartLinePosition.Line;
var endLine = elementSyntax.EndTag.GetLocation().GetLineSpan().EndLinePosition.Line;

if (endLine == startLine)
continue; // Single line, no issue

// Check if content is single-line (ignoring whitespace)
// Skip if content contains CDATA sections or other non-text elements
var hasCDataOrOtherElements = false;
var meaningfulTextTokenCount = 0;
foreach (var content in elementSyntax.Content)
{
if (content is XmlTextSyntax textSyntax)
{
foreach (var token in textSyntax.TextTokens)
{
// Skip whitespace-only tokens and newline tokens
if (token.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.XmlTextLiteralNewLineToken))
continue;

var text = token.Text.Trim();
if (!string.IsNullOrWhiteSpace(text))
{
meaningfulTextTokenCount++;
}
}
}
else if (content is XmlCDataSectionSyntax || content is XmlElementSyntax)
{
// Skip elements with CDATA sections or nested elements
hasCDataOrOtherElements = true;
break;
}
}

// Report diagnostic if content is effectively single-line (0 or 1 meaningful text tokens)
// and doesn't contain CDATA or other nested elements
if (!hasCDataOrOtherElements && meaningfulTextTokenCount <= 1)
{
// Check if the single-line version would fit within max_line_length
if (WouldFitInMaxLineLength(context, elementSyntax))
{
context.ReportDiagnostic(Diagnostic.Create(Rule, elementSyntax.GetLocation()));
}
}
}
}
}
}
}

private static bool WouldFitInMaxLineLength(SymbolAnalysisContext context, XmlElementSyntax elementSyntax)
{
// Get max_line_length from .editorconfig
var options = context.Options.AnalyzerConfigOptionsProvider.GetOptions(elementSyntax.SyntaxTree);
if (!options.TryGetValue("max_line_length", out var maxLineLengthValue))
return true; // No limit configured, allow the change

if (!int.TryParse(maxLineLengthValue, System.Globalization.NumberStyles.None, System.Globalization.CultureInfo.InvariantCulture, out var maxLineLength) || maxLineLength <= 0)
return true; // Invalid or no limit, allow the change

// Get the indentation of the current line
var lineSpan = elementSyntax.GetLocation().GetLineSpan();
var sourceText = elementSyntax.SyntaxTree.GetText();
var line = sourceText.Lines[lineSpan.StartLinePosition.Line];
var lineText = line.ToString();
var indentation = lineText.Length - lineText.TrimStart().Length;

// Build the single-line content
var contentLength = indentation;
var elementName = elementSyntax.StartTag.Name.LocalName.Text;
var attributes = elementSyntax.StartTag.Attributes;

// Calculate: "/// <elementName" + attributes + ">" + content + "</elementName>"
contentLength += 4; // "/// "
contentLength += 1; // "<"
contentLength += elementName.Length;

// Add attribute lengths
foreach (var attribute in attributes)
{
contentLength += attribute.Span.Length + 1; // +1 for space before attribute
}

contentLength += 1; // ">"

// Add text content
var hasContent = false;
foreach (var content in elementSyntax.Content)
{
if (content is XmlTextSyntax textSyntax)
{
foreach (var token in textSyntax.TextTokens)
{
if (token.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.XmlTextLiteralNewLineToken))
continue;

var text = token.Text.Trim();
if (!string.IsNullOrWhiteSpace(text))
{
if (hasContent)
contentLength += 1; // space separator between multiple text tokens
contentLength += text.Length;
hasContent = true;
}
}
}
}

contentLength += 2; // "</"
contentLength += elementName.Length;
contentLength += 1; // ">"

return contentLength <= maxLineLength;
}
}
Loading