From 8503d66daace9c37dcd189daef90e75ee1a0e4a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 21:54:43 +0000 Subject: [PATCH 01/10] Initial plan From ffd9c1e2ea2a4708c244612f86ef3051490b50aa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 22:31:57 +0000 Subject: [PATCH 02/10] Add MA0177 analyzer for XML comments with single-line content Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com> --- ...InlineXmlCommentSyntaxWhenPossibleFixer.cs | 78 +++++++ src/Meziantou.Analyzer/RuleIdentifiers.cs | 1 + ...ineXmlCommentSyntaxWhenPossibleAnalyzer.cs | 114 ++++++++++ ...lCommentSyntaxWhenPossibleAnalyzerTests.cs | 215 ++++++++++++++++++ 4 files changed, 408 insertions(+) create mode 100644 src/Meziantou.Analyzer.CodeFixers/Rules/UseInlineXmlCommentSyntaxWhenPossibleFixer.cs create mode 100644 src/Meziantou.Analyzer/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzer.cs create mode 100644 tests/Meziantou.Analyzer.Test/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzerTests.cs diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/UseInlineXmlCommentSyntaxWhenPossibleFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/UseInlineXmlCommentSyntaxWhenPossibleFixer.cs new file mode 100644 index 00000000..baab19eb --- /dev/null +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/UseInlineXmlCommentSyntaxWhenPossibleFixer.cs @@ -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.Syntax; +using Microsoft.CodeAnalysis.CSharp; +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 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 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(XmlText(contentText.ToString())), + XmlElementEndTag(elementName)) + .WithLeadingTrivia(elementSyntax.GetLeadingTrivia()) + .WithTrailingTrivia(elementSyntax.GetTrailingTrivia()); + + editor.ReplaceNode(elementSyntax, newNode); + return editor.GetChangedDocument(); + } +} diff --git a/src/Meziantou.Analyzer/RuleIdentifiers.cs b/src/Meziantou.Analyzer/RuleIdentifiers.cs index 64a7420b..517d5e02 100755 --- a/src/Meziantou.Analyzer/RuleIdentifiers.cs +++ b/src/Meziantou.Analyzer/RuleIdentifiers.cs @@ -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) { diff --git a/src/Meziantou.Analyzer/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzer.cs new file mode 100644 index 00000000..aa90f4db --- /dev/null +++ b/src/Meziantou.Analyzer/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzer.cs @@ -0,0 +1,114 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Meziantou.Analyzer.Rules; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UseInlineXmlCommentSyntaxWhenPossibleAnalyzer : DiagnosticAnalyzer +{ + private static readonly HashSet RootXmlElements = new(StringComparer.OrdinalIgnoreCase) + { + "summary", + "remarks", + "returns", + "value", + "example", + "param", + "typeparam", + "exception", + }; + + 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 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) + { + var elementName = elementSyntax.StartTag.Name.LocalName.Text; + if (!RootXmlElements.Contains(elementName)) + continue; + + // 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())); + } + } + } + } + } + } +} diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzerTests.cs new file mode 100644 index 00000000..a7b0adf7 --- /dev/null +++ b/tests/Meziantou.Analyzer.Test/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzerTests.cs @@ -0,0 +1,215 @@ +using Meziantou.Analyzer.Rules; +using Meziantou.Analyzer.Test.Helpers; +using TestHelper; + +namespace Meziantou.Analyzer.Test.Rules; + +public sealed class UseInlineXmlCommentSyntaxWhenPossibleAnalyzerTests +{ + private static ProjectBuilder CreateProjectBuilder() + { + return new ProjectBuilder() + .WithAnalyzer() + .WithCodeFixProvider() + .WithTargetFramework(TargetFramework.NetLatest); + } + + [Fact] + public async Task SingleLineDescription_ShouldReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" +/// [| +/// description +/// |] +class Sample { } +""") + .ShouldFixCodeWith(""" +/// description +class Sample { } +""") + .ValidateAsync(); + } + + [Fact] + public async Task MultiLineDescription_ShouldNotReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" +/// +/// description line 1 +/// description line 2 +/// +class Sample { } +""") + .ValidateAsync(); + } + + [Fact] + public async Task AlreadyInline_ShouldNotReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" +/// description +class Sample { } +""") + .ValidateAsync(); + } + + [Fact] + public async Task ParamSingleLine_ShouldReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" +class Sample +{ + /// [| + /// The value + /// |] + public void Method(int value) { } +} +""") + .ShouldFixCodeWith(""" +class Sample +{ + /// The value + public void Method(int value) { } +} +""") + .ValidateAsync(); + } + + [Fact] + public async Task RemarksSingleLine_ShouldReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" +/// [| +/// This is a remark +/// |] +class Sample { } +""") + .ShouldFixCodeWith(""" +/// This is a remark +class Sample { } +""") + .ValidateAsync(); + } + + [Fact] + public async Task ReturnsSingleLine_ShouldReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" +class Sample +{ + /// [| + /// The result + /// |] + public int Method() => 42; +} +""") + .ShouldFixCodeWith(""" +class Sample +{ + /// The result + public int Method() => 42; +} +""") + .ValidateAsync(); + } + + [Fact] + public async Task InnerXmlElements_ShouldNotReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" +/// +/// This has +/// code +/// inside +/// +class Sample { } +""") + .ValidateAsync(); + } + + [Fact] + public async Task EmptyContent_ShouldReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" +/// [| +/// |] +class Sample { } +""") + .ShouldFixCodeWith(""" +/// +class Sample { } +""") + .ValidateAsync(); + } + + [Fact] + public async Task TypeParamSingleLine_ShouldReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" +/// [| +/// The type parameter +/// |] +class Sample { } +""") + .ShouldFixCodeWith(""" +/// The type parameter +class Sample { } +""") + .ValidateAsync(); + } + + [Fact] + public async Task ExceptionSingleLine_ShouldReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" +class Sample +{ + /// [| + /// Thrown when argument is null + /// |] + public void Method(string value) { } +} +""") + .ShouldFixCodeWith(""" +class Sample +{ + /// Thrown when argument is null + public void Method(string value) { } +} +""") + .ValidateAsync(); + } + + [Fact] + public async Task ValueSingleLine_ShouldReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" +class Sample +{ + /// [| + /// The property value + /// |] + public int Property { get; set; } +} +""") + .ShouldFixCodeWith(""" +class Sample +{ + /// The property value + public int Property { get; set; } +} +""") + .ValidateAsync(); + } +} From 9a976da5d7b1dbe5924c130370ad193076c7ed0c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 22:38:10 +0000 Subject: [PATCH 03/10] Changes before error encountered Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com> --- .../Rules/UseInlineXmlCommentSyntaxWhenPossibleFixer.cs | 4 ++-- .../Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzer.cs | 4 ++-- .../UseInlineXmlCommentSyntaxWhenPossibleAnalyzerTests.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/UseInlineXmlCommentSyntaxWhenPossibleFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/UseInlineXmlCommentSyntaxWhenPossibleFixer.cs index baab19eb..7cac4cea 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Rules/UseInlineXmlCommentSyntaxWhenPossibleFixer.cs +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/UseInlineXmlCommentSyntaxWhenPossibleFixer.cs @@ -1,11 +1,11 @@ -using System.Collections.Immutable; +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.Syntax; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Editing; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; diff --git a/src/Meziantou.Analyzer/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzer.cs index aa90f4db..d144f9ca 100644 --- a/src/Meziantou.Analyzer/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzer.cs @@ -1,7 +1,7 @@ -using System.Collections.Immutable; +using System.Collections.Immutable; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; namespace Meziantou.Analyzer.Rules; diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzerTests.cs index a7b0adf7..61401bce 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzerTests.cs @@ -1,4 +1,4 @@ -using Meziantou.Analyzer.Rules; +using Meziantou.Analyzer.Rules; using Meziantou.Analyzer.Test.Helpers; using TestHelper; From 07df1bd2ff2bedb3f4b6c28fa62282cc77e8b67d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 02:13:04 +0000 Subject: [PATCH 04/10] Address code review feedback: remove tag filtering, fix line comparison, indent test raw strings, add edge case tests Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com> --- ...ineXmlCommentSyntaxWhenPossibleAnalyzer.cs | 18 +- ...lCommentSyntaxWhenPossibleAnalyzerTests.cs | 257 +++++++++++------- 2 files changed, 155 insertions(+), 120 deletions(-) diff --git a/src/Meziantou.Analyzer/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzer.cs index d144f9ca..11e30975 100644 --- a/src/Meziantou.Analyzer/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzer.cs @@ -8,18 +8,6 @@ namespace Meziantou.Analyzer.Rules; [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class UseInlineXmlCommentSyntaxWhenPossibleAnalyzer : DiagnosticAnalyzer { - private static readonly HashSet RootXmlElements = new(StringComparer.OrdinalIgnoreCase) - { - "summary", - "remarks", - "returns", - "value", - "example", - "param", - "typeparam", - "exception", - }; - private static readonly DiagnosticDescriptor Rule = new( RuleIdentifiers.UseInlineXmlCommentSyntaxWhenPossible, title: "Use inline XML comment syntax when possible", @@ -68,15 +56,11 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context) { if (childNode is XmlElementSyntax elementSyntax) { - var elementName = elementSyntax.StartTag.Name.LocalName.Text; - if (!RootXmlElements.Contains(elementName)) - continue; - // 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) + if (endLine == startLine) continue; // Single line, no issue // Check if content is single-line (ignoring whitespace) diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzerTests.cs index 61401bce..137e1fc8 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzerTests.cs @@ -19,15 +19,15 @@ public async Task SingleLineDescription_ShouldReportDiagnostic() { await CreateProjectBuilder() .WithSourceCode(""" -/// [| -/// description -/// |] -class Sample { } -""") + /// [| + /// description + /// |] + class Sample { } + """) .ShouldFixCodeWith(""" -/// description -class Sample { } -""") + /// description + class Sample { } + """) .ValidateAsync(); } @@ -36,12 +36,12 @@ public async Task MultiLineDescription_ShouldNotReportDiagnostic() { await CreateProjectBuilder() .WithSourceCode(""" -/// -/// description line 1 -/// description line 2 -/// -class Sample { } -""") + /// + /// description line 1 + /// description line 2 + /// + class Sample { } + """) .ValidateAsync(); } @@ -50,9 +50,9 @@ public async Task AlreadyInline_ShouldNotReportDiagnostic() { await CreateProjectBuilder() .WithSourceCode(""" -/// description -class Sample { } -""") + /// description + class Sample { } + """) .ValidateAsync(); } @@ -61,21 +61,21 @@ public async Task ParamSingleLine_ShouldReportDiagnostic() { await CreateProjectBuilder() .WithSourceCode(""" -class Sample -{ - /// [| - /// The value - /// |] - public void Method(int value) { } -} -""") + class Sample + { + /// [| + /// The value + /// |] + public void Method(int value) { } + } + """) .ShouldFixCodeWith(""" -class Sample -{ - /// The value - public void Method(int value) { } -} -""") + class Sample + { + /// The value + public void Method(int value) { } + } + """) .ValidateAsync(); } @@ -84,15 +84,15 @@ public async Task RemarksSingleLine_ShouldReportDiagnostic() { await CreateProjectBuilder() .WithSourceCode(""" -/// [| -/// This is a remark -/// |] -class Sample { } -""") + /// [| + /// This is a remark + /// |] + class Sample { } + """) .ShouldFixCodeWith(""" -/// This is a remark -class Sample { } -""") + /// This is a remark + class Sample { } + """) .ValidateAsync(); } @@ -101,21 +101,21 @@ public async Task ReturnsSingleLine_ShouldReportDiagnostic() { await CreateProjectBuilder() .WithSourceCode(""" -class Sample -{ - /// [| - /// The result - /// |] - public int Method() => 42; -} -""") + class Sample + { + /// [| + /// The result + /// |] + public int Method() => 42; + } + """) .ShouldFixCodeWith(""" -class Sample -{ - /// The result - public int Method() => 42; -} -""") + class Sample + { + /// The result + public int Method() => 42; + } + """) .ValidateAsync(); } @@ -124,13 +124,13 @@ public async Task InnerXmlElements_ShouldNotReportDiagnostic() { await CreateProjectBuilder() .WithSourceCode(""" -/// -/// This has -/// code -/// inside -/// -class Sample { } -""") + /// + /// This has + /// code + /// inside + /// + class Sample { } + """) .ValidateAsync(); } @@ -139,14 +139,14 @@ public async Task EmptyContent_ShouldReportDiagnostic() { await CreateProjectBuilder() .WithSourceCode(""" -/// [| -/// |] -class Sample { } -""") + /// [| + /// |] + class Sample { } + """) .ShouldFixCodeWith(""" -/// -class Sample { } -""") + /// + class Sample { } + """) .ValidateAsync(); } @@ -155,15 +155,15 @@ public async Task TypeParamSingleLine_ShouldReportDiagnostic() { await CreateProjectBuilder() .WithSourceCode(""" -/// [| -/// The type parameter -/// |] -class Sample { } -""") + /// [| + /// The type parameter + /// |] + class Sample { } + """) .ShouldFixCodeWith(""" -/// The type parameter -class Sample { } -""") + /// The type parameter + class Sample { } + """) .ValidateAsync(); } @@ -172,21 +172,21 @@ public async Task ExceptionSingleLine_ShouldReportDiagnostic() { await CreateProjectBuilder() .WithSourceCode(""" -class Sample -{ - /// [| - /// Thrown when argument is null - /// |] - public void Method(string value) { } -} -""") + class Sample + { + /// [| + /// Thrown when argument is null + /// |] + public void Method(string value) { } + } + """) .ShouldFixCodeWith(""" -class Sample -{ - /// Thrown when argument is null - public void Method(string value) { } -} -""") + class Sample + { + /// Thrown when argument is null + public void Method(string value) { } + } + """) .ValidateAsync(); } @@ -195,21 +195,72 @@ public async Task ValueSingleLine_ShouldReportDiagnostic() { await CreateProjectBuilder() .WithSourceCode(""" -class Sample -{ - /// [| - /// The property value - /// |] - public int Property { get; set; } -} -""") + class Sample + { + /// [| + /// The property value + /// |] + public int Property { get; set; } + } + """) .ShouldFixCodeWith(""" -class Sample -{ - /// The property value - public int Property { get; set; } -} -""") + class Sample + { + /// The property value + public int Property { get; set; } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task ContentOnSameLineAsOpenTag_ShouldReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + /// [|line 1 + /// |] + class Sample { } + """) + .ShouldFixCodeWith(""" + /// line 1 + class Sample { } + """) + .ValidateAsync(); + } + + [Fact] + public async Task ContentOnSameLineAsOpenTagAndCloseTag_ShouldNotReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + /// line 1 + /// line 2 + class Sample { } + """) + .ValidateAsync(); + } + + [Fact] + public async Task CDataSection_ShouldNotReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + /// + class Sample { } + """) + .ValidateAsync(); + } + + [Fact] + public async Task EntityReference_ShouldNotReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + /// line1 line2 + class Sample { } + """) .ValidateAsync(); } } From 42d15afded66501ba2230c85554b1fd413ed0a7f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 02:48:29 +0000 Subject: [PATCH 05/10] Change terminology from 'inline' to 'single-line' XML comment Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com> --- .../Rules/UseInlineXmlCommentSyntaxWhenPossibleFixer.cs | 6 +++--- src/Meziantou.Analyzer/RuleIdentifiers.cs | 2 +- .../UseInlineXmlCommentSyntaxWhenPossibleAnalyzer.cs | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/UseInlineXmlCommentSyntaxWhenPossibleFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/UseInlineXmlCommentSyntaxWhenPossibleFixer.cs index 7cac4cea..d4d65c38 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Rules/UseInlineXmlCommentSyntaxWhenPossibleFixer.cs +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/UseInlineXmlCommentSyntaxWhenPossibleFixer.cs @@ -14,7 +14,7 @@ namespace Meziantou.Analyzer.Rules; [ExportCodeFixProvider(LanguageNames.CSharp), Shared] public sealed class UseInlineXmlCommentSyntaxWhenPossibleFixer : CodeFixProvider { - public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.UseInlineXmlCommentSyntaxWhenPossible); + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.UseSingleLineXmlCommentSyntaxWhenPossible); public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; @@ -25,7 +25,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) if (nodeToFix is not XmlElementSyntax elementSyntax) return; - var title = "Use inline XML comment syntax"; + var title = "Use single-line XML comment syntax"; var codeAction = CodeAction.Create( title, cancellationToken => Fix(context.Document, elementSyntax, cancellationToken), @@ -61,7 +61,7 @@ private static async Task Fix(Document document, XmlElementSyntax elem } } - // Create inline syntax + // Create single-line syntax var elementName = elementSyntax.StartTag.Name; var attributes = elementSyntax.StartTag.Attributes; diff --git a/src/Meziantou.Analyzer/RuleIdentifiers.cs b/src/Meziantou.Analyzer/RuleIdentifiers.cs index 517d5e02..08fa9ad2 100755 --- a/src/Meziantou.Analyzer/RuleIdentifiers.cs +++ b/src/Meziantou.Analyzer/RuleIdentifiers.cs @@ -177,7 +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 const string UseSingleLineXmlCommentSyntaxWhenPossible = "MA0177"; public static string GetHelpUri(string identifier) { diff --git a/src/Meziantou.Analyzer/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzer.cs index 11e30975..d9ba01cb 100644 --- a/src/Meziantou.Analyzer/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzer.cs @@ -9,14 +9,14 @@ namespace Meziantou.Analyzer.Rules; 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", + 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.UseInlineXmlCommentSyntaxWhenPossible)); + helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.UseSingleLineXmlCommentSyntaxWhenPossible)); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); From 965a7f5cc7b33abd983d0a11c6e20f732d087333 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 03:18:08 +0000 Subject: [PATCH 06/10] Add max_line_length check to avoid exceeding line length limits Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com> --- ...ineXmlCommentSyntaxWhenPossibleAnalyzer.cs | 71 ++++++++++++++++++- ...lCommentSyntaxWhenPossibleAnalyzerTests.cs | 49 +++++++++++++ 2 files changed, 119 insertions(+), 1 deletion(-) diff --git a/src/Meziantou.Analyzer/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzer.cs index d9ba01cb..332742ba 100644 --- a/src/Meziantou.Analyzer/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzer.cs @@ -88,11 +88,80 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context) // Report diagnostic if content is effectively single-line (0 or 1 meaningful text tokens) if (meaningfulTextTokenCount <= 1) { - context.ReportDiagnostic(Diagnostic.Create(Rule, elementSyntax.GetLocation())); + // 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: "/// " + content + "" + 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; // "" + + return contentLength <= maxLineLength; + } } diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzerTests.cs index 137e1fc8..18ae4a3d 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzerTests.cs @@ -263,4 +263,53 @@ class Sample { } """) .ValidateAsync(); } + + [Fact] + public async Task MaxLineLength_WouldExceedLimit_ShouldNotReportDiagnostic() + { + await CreateProjectBuilder() + .AddAnalyzerConfiguration("max_line_length", "50") + .WithSourceCode(""" + /// + /// This is a very long description that would exceed the max line length limit + /// + class Sample { } + """) + .ValidateAsync(); + } + + [Fact] + public async Task MaxLineLength_WithinLimit_ShouldReportDiagnostic() + { + await CreateProjectBuilder() + .AddAnalyzerConfiguration("max_line_length", "100") + .WithSourceCode(""" + /// [| + /// Short description + /// |] + class Sample { } + """) + .ShouldFixCodeWith(""" + /// Short description + class Sample { } + """) + .ValidateAsync(); + } + + [Fact] + public async Task MaxLineLength_NotConfigured_ShouldReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + /// [| + /// This is a very long description that could potentially exceed some line length limit + /// |] + class Sample { } + """) + .ShouldFixCodeWith(""" + /// This is a very long description that could potentially exceed some line length limit + class Sample { } + """) + .ValidateAsync(); + } } From 16ec8c161d951d81bd7011cc658bcd15d90f4234 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 07:46:07 +0000 Subject: [PATCH 07/10] Fix analyzer to skip CDATA sections and nested elements Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com> --- .../UseInlineXmlCommentSyntaxWhenPossibleAnalyzer.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Meziantou.Analyzer/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzer.cs index 332742ba..deb80238 100644 --- a/src/Meziantou.Analyzer/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/UseInlineXmlCommentSyntaxWhenPossibleAnalyzer.cs @@ -64,7 +64,8 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context) continue; // Single line, no issue // Check if content is single-line (ignoring whitespace) - // Count the number of text tokens that have meaningful content + // Skip if content contains CDATA sections or other non-text elements + var hasCDataOrOtherElements = false; var meaningfulTextTokenCount = 0; foreach (var content in elementSyntax.Content) { @@ -83,10 +84,17 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context) } } } + 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) - if (meaningfulTextTokenCount <= 1) + // 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)) From 4e46bc2fbf99a17d2dda3f9464c484a59da117e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 14:34:11 +0000 Subject: [PATCH 08/10] Update documentation for MA0177 analyzer Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com> --- README.md | 1 + docs/README.md | 7 +++++++ docs/Rules/MA0177.md | 1 + .../configuration/default.editorconfig | 3 +++ .../configuration/none.editorconfig | 3 +++ 5 files changed, 15 insertions(+) create mode 100644 docs/Rules/MA0177.md diff --git a/README.md b/README.md index 12cd188a..2da19c83 100755 --- a/README.md +++ b/README.md @@ -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|ℹ️|❌|✔️| diff --git a/docs/README.md b/docs/README.md index dd04c0b6..b908fc63 100755 --- a/docs/README.md +++ b/docs/README.md @@ -176,6 +176,7 @@ |[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|ℹ️|❌|✔️| |Id|Suppressed rule|Justification| |--|---------------|-------------| @@ -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 @@ -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 ``` diff --git a/docs/Rules/MA0177.md b/docs/Rules/MA0177.md new file mode 100644 index 00000000..8e2fbef2 --- /dev/null +++ b/docs/Rules/MA0177.md @@ -0,0 +1 @@ +# MA0177 - Use single-line XML comment syntax when possible \ No newline at end of file diff --git a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig index 4995e49d..75332eb9 100644 --- a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig +++ b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig @@ -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 diff --git a/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig index 8172a730..437df117 100644 --- a/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig +++ b/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig @@ -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 From 499e3138523c163bde40d99ff9a89e0b255e8072 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 14:41:16 +0000 Subject: [PATCH 09/10] Add comprehensive documentation for MA0177 rule Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com> --- docs/Rules/MA0177.md | 75 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/docs/Rules/MA0177.md b/docs/Rules/MA0177.md index 8e2fbef2..aaef94c5 100644 --- a/docs/Rules/MA0177.md +++ b/docs/Rules/MA0177.md @@ -1 +1,74 @@ -# MA0177 - Use single-line XML comment syntax when possible \ No newline at end of file +# 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 +/// +/// This is a description +/// +public class Sample { } + +// Should be +/// This is a description +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 ``, ``, 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 +/// +/// Returns the sum of two numbers +/// +public int Add(int a, int b) => a + b; + +// Compliant: Single line +/// Returns the sum of two numbers +public int Add(int a, int b) => a + b; + +// Compliant: Multiple lines of actual content +/// +/// Returns the sum of two numbers. +/// This method handles integer overflow. +/// +public int Add(int a, int b) => a + b; + +// Compliant: Contains nested XML element +/// +/// Returns the sum using method +/// +public int Calculate(int a, int b) => Add(a, b); + +// Compliant: Contains CDATA section +/// +/// ]]> +public void Process() { } +```` \ No newline at end of file From 6571218ac632b771901e814bb820acaa52a36183 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 15:08:21 +0000 Subject: [PATCH 10/10] Rerun documentation generator to fix formatting Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com> --- docs/Rules/MA0177.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Rules/MA0177.md b/docs/Rules/MA0177.md index aaef94c5..215df7ea 100644 --- a/docs/Rules/MA0177.md +++ b/docs/Rules/MA0177.md @@ -71,4 +71,4 @@ public int Calculate(int a, int b) => Add(a, b); /// Special content with /// ]]> public void Process() { } -```` \ No newline at end of file +````