Skip to content

Commit 9fe4dc0

Browse files
authored
Improve fixer for RCS1228 (#1585)
1 parent f15e244 commit 9fe4dc0

File tree

8 files changed

+234
-161
lines changed

8 files changed

+234
-161
lines changed

ChangeLog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Fixed
1111

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

1415
### Changed
1516

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// Copyright (c) .NET Foundation and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
2+
3+
using System;
4+
using System.Collections.Immutable;
5+
using System.Composition;
6+
using System.Linq;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using Microsoft.CodeAnalysis;
10+
using Microsoft.CodeAnalysis.CodeActions;
11+
using Microsoft.CodeAnalysis.CodeFixes;
12+
using Microsoft.CodeAnalysis.CSharp;
13+
using Microsoft.CodeAnalysis.CSharp.Syntax;
14+
using Microsoft.CodeAnalysis.Text;
15+
using Roslynator.CodeFixes;
16+
using Roslynator.CSharp.Syntax;
17+
18+
namespace Roslynator.CSharp.CodeFixes;
19+
20+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(RemoveElementInDocumentationCommentCodeFixProvider))]
21+
[Shared]
22+
public sealed class RemoveElementInDocumentationCommentCodeFixProvider : BaseCodeFixProvider
23+
{
24+
public override ImmutableArray<string> FixableDiagnosticIds
25+
{
26+
get
27+
{
28+
return ImmutableArray.Create(
29+
DiagnosticIdentifiers.UnusedElementInDocumentationComment,
30+
DiagnosticIdentifiers.InvalidReferenceInDocumentationComment);
31+
}
32+
}
33+
34+
#if ROSLYN_4_0
35+
public override FixAllProvider GetFixAllProvider()
36+
{
37+
return FixAllProvider.Create(async (context, document, diagnostics) => await FixAllAsync(document, diagnostics, context.CancellationToken).ConfigureAwait(false));
38+
39+
static async Task<Document> FixAllAsync(
40+
Document document,
41+
ImmutableArray<Diagnostic> diagnostics,
42+
CancellationToken cancellationToken)
43+
{
44+
foreach (Diagnostic diagnostic in diagnostics.OrderByDescending(d => d.Location.SourceSpan.Start))
45+
{
46+
(Func<CancellationToken, Task<Document>> CreateChangedDocument, string) result
47+
= await GetChangedDocumentAsync(document, diagnostic, cancellationToken).ConfigureAwait(false);
48+
49+
document = await result.CreateChangedDocument(cancellationToken).ConfigureAwait(false);
50+
}
51+
52+
return document;
53+
}
54+
}
55+
#endif
56+
57+
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
58+
{
59+
SyntaxNode root = await context.GetSyntaxRootAsync().ConfigureAwait(false);
60+
61+
if (!TryFindFirstAncestorOrSelf(root, context.Span, out XmlNodeSyntax xmlNode, findInsideTrivia: true))
62+
return;
63+
64+
Document document = context.Document;
65+
Diagnostic diagnostic = context.Diagnostics[0];
66+
67+
(Func<CancellationToken, Task<Document>> createChangedDocument, string name)
68+
= await GetChangedDocumentAsync(document, diagnostic, context.CancellationToken).ConfigureAwait(false);
69+
70+
CodeAction codeAction = CodeAction.Create(
71+
$"Remove '{name}' element",
72+
ct => createChangedDocument(ct),
73+
GetEquivalenceKey(diagnostic, name));
74+
75+
context.RegisterCodeFix(codeAction, diagnostic);
76+
}
77+
78+
private static async Task<(Func<CancellationToken, Task<Document>>, string)> GetChangedDocumentAsync(
79+
Document document,
80+
Diagnostic diagnostic,
81+
CancellationToken cancellationToken)
82+
{
83+
SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
84+
85+
if (!TryFindFirstAncestorOrSelf(root, diagnostic.Location.SourceSpan, out XmlNodeSyntax xmlNode, findInsideTrivia: true))
86+
throw new InvalidOperationException();
87+
88+
XmlElementInfo elementInfo = SyntaxInfo.XmlElementInfo(xmlNode);
89+
string name = elementInfo.LocalName;
90+
91+
return (ct => RemoveElementAsync(document, elementInfo, ct), name);
92+
}
93+
94+
private static Task<Document> RemoveElementAsync(
95+
Document document,
96+
in XmlElementInfo elementInfo,
97+
CancellationToken cancellationToken)
98+
{
99+
cancellationToken.ThrowIfCancellationRequested();
100+
101+
XmlNodeSyntax element = elementInfo.Element;
102+
103+
var documentationComment = (DocumentationCommentTriviaSyntax)element.Parent;
104+
105+
SyntaxList<XmlNodeSyntax> content = documentationComment.Content;
106+
107+
if (content.Count(f => f.IsKind(SyntaxKind.XmlElement, SyntaxKind.XmlEmptyElement)) == 1)
108+
{
109+
SyntaxNode declaration = documentationComment
110+
.GetParent(ascendOutOfTrivia: true)
111+
.FirstAncestorOrSelf(f => f is MemberDeclarationSyntax or LocalFunctionStatementSyntax);
112+
113+
SyntaxNode newNode = SyntaxRefactorings.RemoveSingleLineDocumentationComment(declaration, documentationComment);
114+
return document.ReplaceNodeAsync(declaration, newNode, cancellationToken);
115+
}
116+
117+
int start = element.FullSpan.Start;
118+
int end = element.FullSpan.End;
119+
120+
int index = content.IndexOf(element);
121+
122+
if (index > 0
123+
&& content[index - 1].IsKind(SyntaxKind.XmlText))
124+
{
125+
start = content[index - 1].FullSpan.Start;
126+
127+
if (index == 1)
128+
{
129+
SyntaxNode parent = documentationComment.GetParent(ascendOutOfTrivia: true);
130+
SyntaxTriviaList leadingTrivia = parent.GetLeadingTrivia();
131+
132+
index = leadingTrivia.IndexOf(documentationComment.ParentTrivia);
133+
134+
if (index > 0
135+
&& leadingTrivia[index - 1].IsKind(SyntaxKind.WhitespaceTrivia))
136+
{
137+
start = leadingTrivia[index - 1].FullSpan.Start;
138+
}
139+
140+
SyntaxToken token = parent.GetFirstToken().GetPreviousToken(includeDirectives: true);
141+
parent = parent.FirstAncestorOrSelf(f => f.FullSpan.Contains(token.FullSpan));
142+
143+
if (start > 0)
144+
{
145+
SyntaxTrivia trivia = parent.FindTrivia(start - 1, findInsideTrivia: true);
146+
147+
if (trivia.IsKind(SyntaxKind.EndOfLineTrivia)
148+
&& start == trivia.FullSpan.End)
149+
{
150+
start = trivia.FullSpan.Start;
151+
}
152+
}
153+
}
154+
}
155+
156+
return document.WithTextChangeAsync(new TextChange(TextSpan.FromBounds(start, end), ""), cancellationToken);
157+
}
158+
}

src/Analyzers.CodeFixes/CSharp/CodeFixes/XmlNodeCodeFixProvider.cs

Lines changed: 10 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
using Microsoft.CodeAnalysis;
99
using Microsoft.CodeAnalysis.CodeActions;
1010
using Microsoft.CodeAnalysis.CodeFixes;
11-
using Microsoft.CodeAnalysis.CSharp;
1211
using Microsoft.CodeAnalysis.CSharp.Syntax;
1312
using Roslynator.CodeFixes;
1413
using Roslynator.CSharp.Syntax;
@@ -19,16 +18,7 @@ namespace Roslynator.CSharp.CodeFixes;
1918
[Shared]
2019
public sealed class XmlNodeCodeFixProvider : BaseCodeFixProvider
2120
{
22-
public override ImmutableArray<string> FixableDiagnosticIds
23-
{
24-
get
25-
{
26-
return ImmutableArray.Create(
27-
DiagnosticIdentifiers.UnusedElementInDocumentationComment,
28-
DiagnosticIdentifiers.InvalidReferenceInDocumentationComment,
29-
DiagnosticIdentifiers.FixDocumentationCommentTag);
30-
}
31-
}
21+
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(DiagnosticIdentifiers.FixDocumentationCommentTag);
3222

3323
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
3424
{
@@ -38,148 +28,17 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
3828
return;
3929

4030
Document document = context.Document;
31+
Diagnostic diagnostic = context.Diagnostics[0];
32+
XmlElementInfo elementInfo = SyntaxInfo.XmlElementInfo(xmlNode);
4133

42-
foreach (Diagnostic diagnostic in context.Diagnostics)
43-
{
44-
switch (diagnostic.Id)
45-
{
46-
case DiagnosticIdentifiers.UnusedElementInDocumentationComment:
47-
case DiagnosticIdentifiers.InvalidReferenceInDocumentationComment:
48-
{
49-
XmlElementInfo elementInfo = SyntaxInfo.XmlElementInfo(xmlNode);
50-
51-
string name = elementInfo.LocalName;
52-
53-
CodeAction codeAction = CodeAction.Create(
54-
$"Remove '{name}' element",
55-
ct => RemoveUnusedElementInDocumentationCommentAsync(document, elementInfo, ct),
56-
GetEquivalenceKey(diagnostic, name));
57-
58-
context.RegisterCodeFix(codeAction, diagnostic);
59-
break;
60-
}
61-
case DiagnosticIdentifiers.FixDocumentationCommentTag:
62-
{
63-
XmlElementInfo elementInfo = SyntaxInfo.XmlElementInfo(xmlNode);
64-
65-
CodeAction codeAction = CodeAction.Create(
66-
(elementInfo.GetTag() == XmlTag.C)
67-
? "Rename tag to 'code'"
68-
: "Rename tag to 'c'",
69-
ct => FixDocumentationCommentTagAsync(document, elementInfo, ct),
70-
GetEquivalenceKey(diagnostic));
71-
72-
context.RegisterCodeFix(codeAction, diagnostic);
73-
break;
74-
}
75-
}
76-
}
77-
}
78-
79-
private static Task<Document> RemoveUnusedElementInDocumentationCommentAsync(
80-
Document document,
81-
in XmlElementInfo elementInfo,
82-
CancellationToken cancellationToken)
83-
{
84-
cancellationToken.ThrowIfCancellationRequested();
85-
86-
XmlNodeSyntax element = elementInfo.Element;
87-
88-
var documentationComment = (DocumentationCommentTriviaSyntax)element.Parent;
89-
90-
SyntaxList<XmlNodeSyntax> content = documentationComment.Content;
91-
92-
int count = content.Count;
93-
int index = content.IndexOf(element);
94-
95-
if (index == 0)
96-
{
97-
if (count == 2
98-
&& content[1] is XmlTextSyntax xmlText
99-
&& IsNewLine(xmlText))
100-
{
101-
return document.RemoveSingleLineDocumentationComment(documentationComment, cancellationToken);
102-
}
103-
104-
if (content[index + 1] is XmlTextSyntax xmlText2
105-
&& IsXmlTextBetweenLines(xmlText2))
106-
{
107-
return document.RemoveNodesAsync(new XmlNodeSyntax[] { element, xmlText2 }, SyntaxRefactorings.DefaultRemoveOptions, cancellationToken);
108-
}
109-
}
110-
else if (index == 1)
111-
{
112-
if (count == 3
113-
&& content[0] is XmlTextSyntax xmlText
114-
&& IsWhitespace(xmlText)
115-
&& content[2] is XmlTextSyntax xmlText2
116-
&& IsNewLine(xmlText2))
117-
{
118-
return document.RemoveSingleLineDocumentationComment(documentationComment, cancellationToken);
119-
}
120-
121-
if (content[2] is XmlTextSyntax xmlText3
122-
&& IsXmlTextBetweenLines(xmlText3))
123-
{
124-
return document.RemoveNodesAsync(new XmlNodeSyntax[] { element, xmlText3 }, SyntaxRefactorings.DefaultRemoveOptions, cancellationToken);
125-
}
126-
}
127-
else if (content[index - 1] is XmlTextSyntax xmlText
128-
&& IsXmlTextBetweenLines(xmlText))
129-
{
130-
return document.RemoveNodesAsync(new XmlNodeSyntax[] { xmlText, element }, SyntaxRefactorings.DefaultRemoveOptions, cancellationToken);
131-
}
132-
133-
return document.RemoveNodeAsync(element, cancellationToken);
134-
135-
static bool IsXmlTextBetweenLines(XmlTextSyntax xmlText)
136-
{
137-
SyntaxTokenList tokens = xmlText.TextTokens;
138-
139-
SyntaxTokenList.Enumerator en = tokens.GetEnumerator();
140-
141-
if (!en.MoveNext())
142-
return false;
143-
144-
if (IsEmptyOrWhitespace(en.Current)
145-
&& !en.MoveNext())
146-
{
147-
return false;
148-
}
34+
CodeAction codeAction = CodeAction.Create(
35+
(elementInfo.GetTag() == XmlTag.C)
36+
? "Rename tag to 'code'"
37+
: "Rename tag to 'c'",
38+
ct => FixDocumentationCommentTagAsync(document, elementInfo, ct),
39+
GetEquivalenceKey(diagnostic));
14940

150-
if (!en.Current.IsKind(SyntaxKind.XmlTextLiteralNewLineToken))
151-
return false;
152-
153-
if (en.MoveNext())
154-
{
155-
if (!IsEmptyOrWhitespace(en.Current))
156-
return false;
157-
158-
if (en.MoveNext())
159-
return false;
160-
}
161-
162-
return true;
163-
164-
static bool IsEmptyOrWhitespace(SyntaxToken token)
165-
{
166-
return token.IsKind(SyntaxKind.XmlTextLiteralToken)
167-
&& StringUtility.IsEmptyOrWhitespace(token.ValueText);
168-
}
169-
}
170-
171-
static bool IsWhitespace(XmlTextSyntax xmlText)
172-
{
173-
string text = xmlText.TextTokens.SingleOrDefault(shouldThrow: false).ValueText;
174-
175-
return text.Length > 0
176-
&& StringUtility.IsEmptyOrWhitespace(text);
177-
}
178-
179-
static bool IsNewLine(XmlTextSyntax xmlText)
180-
{
181-
return xmlText.TextTokens.SingleOrDefault(shouldThrow: false).IsKind(SyntaxKind.XmlTextLiteralNewLineToken);
182-
}
41+
context.RegisterCodeFix(codeAction, diagnostic);
18342
}
18443

18544
private static Task<Document> FixDocumentationCommentTagAsync(

src/Analyzers/CSharp/Analysis/AddParagraphToDocumentationCommentAnalyzer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ private static void AnalyzeSingleLineDocumentationCommentTrivia(SyntaxNodeAnalys
4141
{
4242
var documentationComment = (DocumentationCommentTriviaSyntax)context.Node;
4343

44-
if (!documentationComment.IsPartOfMemberDeclaration())
44+
if (!documentationComment.IsPartOfDeclaration())
4545
return;
4646

4747
foreach (XmlNodeSyntax node in documentationComment.Content)

src/Analyzers/CSharp/Analysis/SingleLineDocumentationCommentTriviaAnalyzer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ private static void AnalyzeSingleLineDocumentationCommentTrivia(SyntaxNodeAnalys
5656
{
5757
var documentationComment = (DocumentationCommentTriviaSyntax)context.Node;
5858

59-
if (!documentationComment.IsPartOfMemberDeclaration())
59+
if (!documentationComment.IsPartOfDeclaration())
6060
return;
6161

6262
bool? fixDocumentationCommentTagEnabled = null;

src/CSharp/CSharp/Extensions/SyntaxExtensions.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -622,12 +622,12 @@ internal static IEnumerable<XmlElementSyntax> Elements(this DocumentationComment
622622
}
623623
}
624624

625-
internal static bool IsPartOfMemberDeclaration(this DocumentationCommentTriviaSyntax documentationComment)
625+
internal static bool IsPartOfDeclaration(this DocumentationCommentTriviaSyntax documentationComment)
626626
{
627627
SyntaxNode? node = documentationComment.ParentTrivia.Token.Parent;
628628

629-
return node is MemberDeclarationSyntax
630-
|| node?.Parent is MemberDeclarationSyntax;
629+
return node is MemberDeclarationSyntax or LocalFunctionStatementSyntax
630+
|| node?.Parent is MemberDeclarationSyntax or LocalFunctionStatementSyntax;
631631
}
632632
#endregion DocumentationCommentTriviaSyntax
633633

0 commit comments

Comments
 (0)