Skip to content

Commit

Permalink
Improve fixer for RCS1228 (#1585)
Browse files Browse the repository at this point in the history
  • Loading branch information
josefpihrt authored Nov 23, 2024
1 parent f15e244 commit 9fe4dc0
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 161 deletions.
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

0 comments on commit 9fe4dc0

Please sign in to comment.