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
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.UseInlineXmlCommentSyntaxWhenPossible);

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 inline 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 inline 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();
}
}
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 UseInlineXmlCommentSyntaxWhenPossible = "MA0177";

public static string GetHelpUri(string identifier)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
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.UseInlineXmlCommentSyntaxWhenPossible,
title: "Use inline XML comment syntax when possible",
messageFormat: "Use inline XML comment syntax when possible",
RuleCategories.Style,
DiagnosticSeverity.Info,
isEnabledByDefault: false,
description: "",
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.UseInlineXmlCommentSyntaxWhenPossible));

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)
// Count the number of text tokens that have meaningful content
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++;
}
}
}
}

// Report diagnostic if content is effectively single-line (0 or 1 meaningful text tokens)
if (meaningfulTextTokenCount <= 1)
{
context.ReportDiagnostic(Diagnostic.Create(Rule, elementSyntax.GetLocation()));
}
}
}
}
}
}
}
Loading