Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve fixer for RCS1228 #1585

Merged
merged 15 commits into from
Nov 23, 2024
1 change: 1 addition & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- Fix analyzer [RCS1213](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1213) ([PR](https://github.com/dotnet/roslynator/pull/1586))
- Improve code fixer for [RCS1228](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1228) ([PR](https://github.com/dotnet/roslynator/pull/1585))

### Changed

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// Copyright (c) .NET Foundation and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using Roslynator.CodeFixes;
using Roslynator.CSharp.Syntax;

namespace Roslynator.CSharp.CodeFixes;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(RemoveElementInDocumentationCommentCodeFixProvider))]
[Shared]
public sealed class RemoveElementInDocumentationCommentCodeFixProvider : BaseCodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds
{
get
{
return ImmutableArray.Create(
DiagnosticIdentifiers.UnusedElementInDocumentationComment,
DiagnosticIdentifiers.InvalidReferenceInDocumentationComment);
}
}

#if ROSLYN_4_0
public override FixAllProvider GetFixAllProvider()
{
return FixAllProvider.Create(async (context, document, diagnostics) => await FixAllAsync(document, diagnostics, context.CancellationToken).ConfigureAwait(false));

static async Task<Document> FixAllAsync(
Document document,
ImmutableArray<Diagnostic> diagnostics,
CancellationToken cancellationToken)
{
foreach (Diagnostic diagnostic in diagnostics.OrderByDescending(d => d.Location.SourceSpan.Start))
{
(Func<CancellationToken, Task<Document>> CreateChangedDocument, string) result
= await GetChangedDocumentAsync(document, diagnostic, cancellationToken).ConfigureAwait(false);

document = await result.CreateChangedDocument(cancellationToken).ConfigureAwait(false);
}

return document;
}
}
#endif

public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
SyntaxNode root = await context.GetSyntaxRootAsync().ConfigureAwait(false);

if (!TryFindFirstAncestorOrSelf(root, context.Span, out XmlNodeSyntax xmlNode, findInsideTrivia: true))
return;

Document document = context.Document;
Diagnostic diagnostic = context.Diagnostics[0];

(Func<CancellationToken, Task<Document>> createChangedDocument, string name)
= await GetChangedDocumentAsync(document, diagnostic, context.CancellationToken).ConfigureAwait(false);

CodeAction codeAction = CodeAction.Create(
$"Remove '{name}' element",
ct => createChangedDocument(ct),
GetEquivalenceKey(diagnostic, name));

context.RegisterCodeFix(codeAction, diagnostic);
}

private static async Task<(Func<CancellationToken, Task<Document>>, string)> GetChangedDocumentAsync(
Document document,
Diagnostic diagnostic,
CancellationToken cancellationToken)
{
SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

if (!TryFindFirstAncestorOrSelf(root, diagnostic.Location.SourceSpan, out XmlNodeSyntax xmlNode, findInsideTrivia: true))
throw new InvalidOperationException();

XmlElementInfo elementInfo = SyntaxInfo.XmlElementInfo(xmlNode);
string name = elementInfo.LocalName;

return (ct => RemoveElementAsync(document, elementInfo, ct), name);
}

private static Task<Document> RemoveElementAsync(
Document document,
in XmlElementInfo elementInfo,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

XmlNodeSyntax element = elementInfo.Element;

var documentationComment = (DocumentationCommentTriviaSyntax)element.Parent;

SyntaxList<XmlNodeSyntax> content = documentationComment.Content;

if (content.Count(f => f.IsKind(SyntaxKind.XmlElement, SyntaxKind.XmlEmptyElement)) == 1)
{
SyntaxNode declaration = documentationComment
.GetParent(ascendOutOfTrivia: true)
.FirstAncestorOrSelf(f => f is MemberDeclarationSyntax or LocalFunctionStatementSyntax);

SyntaxNode newNode = SyntaxRefactorings.RemoveSingleLineDocumentationComment(declaration, documentationComment);
return document.ReplaceNodeAsync(declaration, newNode, cancellationToken);
}

int start = element.FullSpan.Start;
int end = element.FullSpan.End;

int index = content.IndexOf(element);

if (index > 0
&& content[index - 1].IsKind(SyntaxKind.XmlText))
{
start = content[index - 1].FullSpan.Start;

if (index == 1)
{
SyntaxNode parent = documentationComment.GetParent(ascendOutOfTrivia: true);
SyntaxTriviaList leadingTrivia = parent.GetLeadingTrivia();

index = leadingTrivia.IndexOf(documentationComment.ParentTrivia);

if (index > 0
&& leadingTrivia[index - 1].IsKind(SyntaxKind.WhitespaceTrivia))
{
start = leadingTrivia[index - 1].FullSpan.Start;
}

SyntaxToken token = parent.GetFirstToken().GetPreviousToken(includeDirectives: true);
parent = parent.FirstAncestorOrSelf(f => f.FullSpan.Contains(token.FullSpan));

if (start > 0)
{
SyntaxTrivia trivia = parent.FindTrivia(start - 1, findInsideTrivia: true);

if (trivia.IsKind(SyntaxKind.EndOfLineTrivia)
&& start == trivia.FullSpan.End)
{
start = trivia.FullSpan.Start;
}
}
}
}

return document.WithTextChangeAsync(new TextChange(TextSpan.FromBounds(start, end), ""), cancellationToken);
}
}
161 changes: 10 additions & 151 deletions src/Analyzers.CodeFixes/CSharp/CodeFixes/XmlNodeCodeFixProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Roslynator.CodeFixes;
using Roslynator.CSharp.Syntax;
Expand All @@ -19,16 +18,7 @@ namespace Roslynator.CSharp.CodeFixes;
[Shared]
public sealed class XmlNodeCodeFixProvider : BaseCodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds
{
get
{
return ImmutableArray.Create(
DiagnosticIdentifiers.UnusedElementInDocumentationComment,
DiagnosticIdentifiers.InvalidReferenceInDocumentationComment,
DiagnosticIdentifiers.FixDocumentationCommentTag);
}
}
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(DiagnosticIdentifiers.FixDocumentationCommentTag);

public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
Expand All @@ -38,148 +28,17 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
return;

Document document = context.Document;
Diagnostic diagnostic = context.Diagnostics[0];
XmlElementInfo elementInfo = SyntaxInfo.XmlElementInfo(xmlNode);

foreach (Diagnostic diagnostic in context.Diagnostics)
{
switch (diagnostic.Id)
{
case DiagnosticIdentifiers.UnusedElementInDocumentationComment:
case DiagnosticIdentifiers.InvalidReferenceInDocumentationComment:
{
XmlElementInfo elementInfo = SyntaxInfo.XmlElementInfo(xmlNode);

string name = elementInfo.LocalName;

CodeAction codeAction = CodeAction.Create(
$"Remove '{name}' element",
ct => RemoveUnusedElementInDocumentationCommentAsync(document, elementInfo, ct),
GetEquivalenceKey(diagnostic, name));

context.RegisterCodeFix(codeAction, diagnostic);
break;
}
case DiagnosticIdentifiers.FixDocumentationCommentTag:
{
XmlElementInfo elementInfo = SyntaxInfo.XmlElementInfo(xmlNode);

CodeAction codeAction = CodeAction.Create(
(elementInfo.GetTag() == XmlTag.C)
? "Rename tag to 'code'"
: "Rename tag to 'c'",
ct => FixDocumentationCommentTagAsync(document, elementInfo, ct),
GetEquivalenceKey(diagnostic));

context.RegisterCodeFix(codeAction, diagnostic);
break;
}
}
}
}

private static Task<Document> RemoveUnusedElementInDocumentationCommentAsync(
Document document,
in XmlElementInfo elementInfo,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

XmlNodeSyntax element = elementInfo.Element;

var documentationComment = (DocumentationCommentTriviaSyntax)element.Parent;

SyntaxList<XmlNodeSyntax> content = documentationComment.Content;

int count = content.Count;
int index = content.IndexOf(element);

if (index == 0)
{
if (count == 2
&& content[1] is XmlTextSyntax xmlText
&& IsNewLine(xmlText))
{
return document.RemoveSingleLineDocumentationComment(documentationComment, cancellationToken);
}

if (content[index + 1] is XmlTextSyntax xmlText2
&& IsXmlTextBetweenLines(xmlText2))
{
return document.RemoveNodesAsync(new XmlNodeSyntax[] { element, xmlText2 }, SyntaxRefactorings.DefaultRemoveOptions, cancellationToken);
}
}
else if (index == 1)
{
if (count == 3
&& content[0] is XmlTextSyntax xmlText
&& IsWhitespace(xmlText)
&& content[2] is XmlTextSyntax xmlText2
&& IsNewLine(xmlText2))
{
return document.RemoveSingleLineDocumentationComment(documentationComment, cancellationToken);
}

if (content[2] is XmlTextSyntax xmlText3
&& IsXmlTextBetweenLines(xmlText3))
{
return document.RemoveNodesAsync(new XmlNodeSyntax[] { element, xmlText3 }, SyntaxRefactorings.DefaultRemoveOptions, cancellationToken);
}
}
else if (content[index - 1] is XmlTextSyntax xmlText
&& IsXmlTextBetweenLines(xmlText))
{
return document.RemoveNodesAsync(new XmlNodeSyntax[] { xmlText, element }, SyntaxRefactorings.DefaultRemoveOptions, cancellationToken);
}

return document.RemoveNodeAsync(element, cancellationToken);

static bool IsXmlTextBetweenLines(XmlTextSyntax xmlText)
{
SyntaxTokenList tokens = xmlText.TextTokens;

SyntaxTokenList.Enumerator en = tokens.GetEnumerator();

if (!en.MoveNext())
return false;

if (IsEmptyOrWhitespace(en.Current)
&& !en.MoveNext())
{
return false;
}
CodeAction codeAction = CodeAction.Create(
(elementInfo.GetTag() == XmlTag.C)
? "Rename tag to 'code'"
: "Rename tag to 'c'",
ct => FixDocumentationCommentTagAsync(document, elementInfo, ct),
GetEquivalenceKey(diagnostic));

if (!en.Current.IsKind(SyntaxKind.XmlTextLiteralNewLineToken))
return false;

if (en.MoveNext())
{
if (!IsEmptyOrWhitespace(en.Current))
return false;

if (en.MoveNext())
return false;
}

return true;

static bool IsEmptyOrWhitespace(SyntaxToken token)
{
return token.IsKind(SyntaxKind.XmlTextLiteralToken)
&& StringUtility.IsEmptyOrWhitespace(token.ValueText);
}
}

static bool IsWhitespace(XmlTextSyntax xmlText)
{
string text = xmlText.TextTokens.SingleOrDefault(shouldThrow: false).ValueText;

return text.Length > 0
&& StringUtility.IsEmptyOrWhitespace(text);
}

static bool IsNewLine(XmlTextSyntax xmlText)
{
return xmlText.TextTokens.SingleOrDefault(shouldThrow: false).IsKind(SyntaxKind.XmlTextLiteralNewLineToken);
}
context.RegisterCodeFix(codeAction, diagnostic);
}

private static Task<Document> FixDocumentationCommentTagAsync(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ private static void AnalyzeSingleLineDocumentationCommentTrivia(SyntaxNodeAnalys
{
var documentationComment = (DocumentationCommentTriviaSyntax)context.Node;

if (!documentationComment.IsPartOfMemberDeclaration())
if (!documentationComment.IsPartOfDeclaration())
return;

foreach (XmlNodeSyntax node in documentationComment.Content)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ private static void AnalyzeSingleLineDocumentationCommentTrivia(SyntaxNodeAnalys
{
var documentationComment = (DocumentationCommentTriviaSyntax)context.Node;

if (!documentationComment.IsPartOfMemberDeclaration())
if (!documentationComment.IsPartOfDeclaration())
return;

bool? fixDocumentationCommentTagEnabled = null;
Expand Down
6 changes: 3 additions & 3 deletions src/CSharp/CSharp/Extensions/SyntaxExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -622,12 +622,12 @@ internal static IEnumerable<XmlElementSyntax> Elements(this DocumentationComment
}
}

internal static bool IsPartOfMemberDeclaration(this DocumentationCommentTriviaSyntax documentationComment)
internal static bool IsPartOfDeclaration(this DocumentationCommentTriviaSyntax documentationComment)
{
SyntaxNode? node = documentationComment.ParentTrivia.Token.Parent;

return node is MemberDeclarationSyntax
|| node?.Parent is MemberDeclarationSyntax;
return node is MemberDeclarationSyntax or LocalFunctionStatementSyntax
|| node?.Parent is MemberDeclarationSyntax or LocalFunctionStatementSyntax;
}
#endregion DocumentationCommentTriviaSyntax

Expand Down
Loading