Skip to content
This repository was archived by the owner on Apr 8, 2019. It is now read-only.

Commit b024b1f

Browse files
committed
Update DeclarePublicAPIAnalyzer to dotnet/roslyn-analyzers@7e75679a
1 parent 4e3311b commit b024b1f

File tree

3 files changed

+738
-369
lines changed

3 files changed

+738
-369
lines changed

PublicApiAnalyzer/PublicApiAnalyzer.CodeFixes/ApiDesign/DeclarePublicAPIFix.cs

Lines changed: 138 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,24 @@
1-
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
2-
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3-
4-
namespace PublicApiAnalyzer.ApiDesign
1+
// Copyright (c) Microsoft. All Rights Reserved. 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.Generic;
5+
using System.Collections.Immutable;
6+
using System.Composition;
7+
using System.Diagnostics;
8+
using System.Linq;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
using Microsoft.CodeAnalysis;
12+
using Microsoft.CodeAnalysis.CodeActions;
13+
using Microsoft.CodeAnalysis.CodeFixes;
14+
using Microsoft.CodeAnalysis.Text;
15+
16+
namespace Roslyn.Diagnostics.Analyzers
517
{
6-
using System;
7-
using System.Collections.Generic;
8-
using System.Collections.Immutable;
9-
using System.Composition;
10-
using System.Linq;
11-
using System.Threading;
12-
using System.Threading.Tasks;
13-
using Microsoft.CodeAnalysis;
14-
using Microsoft.CodeAnalysis.CodeActions;
15-
using Microsoft.CodeAnalysis.CodeFixes;
16-
using Microsoft.CodeAnalysis.Text;
17-
18-
[ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic, Name = "DeclarePublicAPIFix")]
19-
[Shared]
20-
internal class DeclarePublicAPIFix : CodeFixProvider
18+
[ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic, Name = "DeclarePublicAPIFix"), Shared]
19+
public sealed class DeclarePublicAPIFix : CodeFixProvider
2120
{
22-
public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } =
23-
ImmutableArray.Create(RoslynDiagnosticIds.DeclarePublicApiRuleId);
21+
public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(RoslynDiagnosticIds.DeclarePublicApiRuleId);
2422

2523
public sealed override FixAllProvider GetFixAllProvider()
2624
{
@@ -29,26 +27,28 @@ public sealed override FixAllProvider GetFixAllProvider()
2927

3028
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
3129
{
32-
var project = context.Document.Project;
30+
Project project = context.Document.Project;
3331
TextDocument publicSurfaceAreaDocument = GetPublicSurfaceAreaDocument(project);
3432
if (publicSurfaceAreaDocument == null)
3533
{
3634
return;
3735
}
3836

39-
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
40-
var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
41-
foreach (var diagnostic in context.Diagnostics)
37+
SyntaxNode root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
38+
SemanticModel semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
39+
foreach (Diagnostic diagnostic in context.Diagnostics)
4240
{
4341
string minimalSymbolName = diagnostic.Properties[DeclarePublicAPIAnalyzer.MinimalNamePropertyBagKey];
4442
string publicSurfaceAreaSymbolName = diagnostic.Properties[DeclarePublicAPIAnalyzer.PublicApiNamePropertyBagKey];
43+
ImmutableHashSet<string> siblingSymbolNamesToRemove = diagnostic.Properties[DeclarePublicAPIAnalyzer.PublicApiNamesOfSiblingsToRemovePropertyBagKey]
44+
.Split(DeclarePublicAPIAnalyzer.PublicApiNamesOfSiblingsToRemovePropertyBagValueSeparator.ToCharArray())
45+
.ToImmutableHashSet();
4546

4647
context.RegisterCodeFix(
47-
CodeAction.Create(
48-
$"Add '{minimalSymbolName}' to public API",
49-
c => this.GetFixAsync(publicSurfaceAreaDocument, publicSurfaceAreaSymbolName, c),
50-
nameof(DeclarePublicAPIFix)),
51-
diagnostic);
48+
new AdditionalDocumentChangeAction(
49+
$"Add {minimalSymbolName} to public API",
50+
c => GetFix(publicSurfaceAreaDocument, publicSurfaceAreaSymbolName, siblingSymbolNamesToRemove, c)),
51+
diagnostic);
5252
}
5353
}
5454

@@ -57,91 +57,107 @@ private static TextDocument GetPublicSurfaceAreaDocument(Project project)
5757
return project.AdditionalDocuments.FirstOrDefault(doc => doc.Name.Equals(DeclarePublicAPIAnalyzer.UnshippedFileName, StringComparison.Ordinal));
5858
}
5959

60+
private async Task<Solution> GetFix(TextDocument publicSurfaceAreaDocument, string newSymbolName, ImmutableHashSet<string> siblingSymbolNamesToRemove, CancellationToken cancellationToken)
61+
{
62+
SourceText sourceText = await publicSurfaceAreaDocument.GetTextAsync(cancellationToken).ConfigureAwait(false);
63+
SourceText newSourceText = AddSymbolNamesToSourceText(sourceText, new[] { newSymbolName });
64+
newSourceText = RemoveSymbolNamesFromSourceText(newSourceText, siblingSymbolNamesToRemove);
65+
66+
return publicSurfaceAreaDocument.Project.Solution.WithAdditionalDocumentText(publicSurfaceAreaDocument.Id, newSourceText);
67+
}
68+
6069
private static SourceText AddSymbolNamesToSourceText(SourceText sourceText, IEnumerable<string> newSymbolNames)
6170
{
6271
HashSet<string> lines = GetLinesFromSourceText(sourceText);
6372

64-
foreach (var name in newSymbolNames)
73+
foreach (string name in newSymbolNames)
6574
{
6675
lines.Add(name);
6776
}
6877

69-
var sortedLines = lines.OrderBy(s => s, StringComparer.Ordinal);
78+
IOrderedEnumerable<string> sortedLines = lines.OrderBy(s => s, StringComparer.Ordinal);
7079

71-
var newSourceText = sourceText.Replace(new TextSpan(0, sourceText.Length), string.Join(Environment.NewLine, sortedLines));
80+
SourceText newSourceText = sourceText.Replace(new TextSpan(0, sourceText.Length), string.Join(Environment.NewLine, sortedLines) + GetEndOfFileText(sourceText));
7281
return newSourceText;
7382
}
7483

75-
private static HashSet<string> GetLinesFromSourceText(SourceText sourceText)
84+
private static SourceText RemoveSymbolNamesFromSourceText(SourceText sourceText, ImmutableHashSet<string> linesToRemove)
7685
{
77-
var lines = new HashSet<string>();
78-
79-
foreach (var textLine in sourceText.Lines)
86+
if (linesToRemove.IsEmpty)
8087
{
81-
var text = textLine.ToString();
82-
if (!string.IsNullOrWhiteSpace(text))
83-
{
84-
lines.Add(text);
85-
}
88+
return sourceText;
8689
}
8790

88-
return lines;
91+
HashSet<string> lines = GetLinesFromSourceText(sourceText);
92+
var newLines = lines.Where(line => !linesToRemove.Contains(line));
93+
94+
IOrderedEnumerable<string> sortedLines = newLines.OrderBy(s => s, StringComparer.Ordinal);
95+
96+
SourceText newSourceText = sourceText.Replace(new TextSpan(0, sourceText.Length), string.Join(Environment.NewLine, sortedLines) + GetEndOfFileText(sourceText));
97+
return newSourceText;
8998
}
9099

91-
private static ISymbol FindDeclaration(SyntaxNode root, Location location, SemanticModel semanticModel, CancellationToken cancellationToken)
100+
private static HashSet<string> GetLinesFromSourceText(SourceText sourceText)
92101
{
93-
var node = root.FindNode(location.SourceSpan);
94-
ISymbol symbol = null;
95-
while (node != null)
102+
var lines = new HashSet<string>();
103+
104+
foreach (TextLine textLine in sourceText.Lines)
96105
{
97-
symbol = semanticModel.GetDeclaredSymbol(node, cancellationToken);
98-
if (symbol != null)
106+
string text = textLine.ToString();
107+
if (!string.IsNullOrWhiteSpace(text))
99108
{
100-
break;
109+
lines.Add(text);
101110
}
102-
103-
node = node.Parent;
104111
}
105112

106-
return symbol;
113+
return lines;
107114
}
108115

109-
private async Task<Solution> GetFixAsync(TextDocument publicSurfaceAreaDocument, string newSymbolName, CancellationToken cancellationToken)
116+
/// <summary>
117+
/// Returns the trailing newline from the end of <paramref name="sourceText"/>, if one exists.
118+
/// </summary>
119+
/// <param name="sourceText">The source text.</param>
120+
/// <returns><see cref="Environment.NewLine"/> if <paramref name="sourceText"/> ends with a trailing newline;
121+
/// otherwise, <see cref="string.Empty"/>.</returns>
122+
public static string GetEndOfFileText(SourceText sourceText)
110123
{
111-
var sourceText = await publicSurfaceAreaDocument.GetTextAsync(cancellationToken).ConfigureAwait(false);
112-
var newSourceText = AddSymbolNamesToSourceText(sourceText, new[] { newSymbolName });
124+
if (sourceText.Length == 0)
125+
return string.Empty;
113126

114-
return publicSurfaceAreaDocument.Project.Solution.WithAdditionalDocumentText(publicSurfaceAreaDocument.Id, newSourceText);
127+
var lastLine = sourceText.Lines[sourceText.Lines.Count - 1];
128+
return lastLine.Span.IsEmpty ? Environment.NewLine : string.Empty;
115129
}
116130

117131
private class AdditionalDocumentChangeAction : CodeAction
118132
{
119-
private readonly Func<CancellationToken, Task<Solution>> createChangedAdditionalDocument;
133+
private readonly Func<CancellationToken, Task<Solution>> _createChangedAdditionalDocument;
120134

121135
public AdditionalDocumentChangeAction(string title, Func<CancellationToken, Task<Solution>> createChangedAdditionalDocument)
122136
{
123137
this.Title = title;
124-
this.createChangedAdditionalDocument = createChangedAdditionalDocument;
138+
_createChangedAdditionalDocument = createChangedAdditionalDocument;
125139
}
126140

127141
public override string Title { get; }
128142

143+
public override string EquivalenceKey => Title;
144+
129145
protected override Task<Solution> GetChangedSolutionAsync(CancellationToken cancellationToken)
130146
{
131-
return this.createChangedAdditionalDocument(cancellationToken);
147+
return _createChangedAdditionalDocument(cancellationToken);
132148
}
133149
}
134150

135151
private class FixAllAdditionalDocumentChangeAction : CodeAction
136152
{
137-
private readonly List<KeyValuePair<Project, ImmutableArray<Diagnostic>>> diagnosticsToFix;
138-
private readonly Solution solution;
153+
private readonly List<KeyValuePair<Project, ImmutableArray<Diagnostic>>> _diagnosticsToFix;
154+
private readonly Solution _solution;
139155

140156
public FixAllAdditionalDocumentChangeAction(string title, Solution solution, List<KeyValuePair<Project, ImmutableArray<Diagnostic>>> diagnosticsToFix)
141157
{
142158
this.Title = title;
143-
this.solution = solution;
144-
this.diagnosticsToFix = diagnosticsToFix;
159+
_solution = solution;
160+
_diagnosticsToFix = diagnosticsToFix;
145161
}
146162

147163
public override string Title { get; }
@@ -150,55 +166,72 @@ protected override async Task<Solution> GetChangedSolutionAsync(CancellationToke
150166
{
151167
var updatedPublicSurfaceAreaText = new List<KeyValuePair<DocumentId, SourceText>>();
152168

153-
foreach (var pair in this.diagnosticsToFix)
169+
foreach (KeyValuePair<Project, ImmutableArray<Diagnostic>> pair in _diagnosticsToFix)
154170
{
155-
var project = pair.Key;
156-
var diagnostics = pair.Value;
171+
Project project = pair.Key;
172+
ImmutableArray<Diagnostic> diagnostics = pair.Value;
157173

158-
var publicSurfaceAreaAdditionalDocument = GetPublicSurfaceAreaDocument(project);
174+
TextDocument publicSurfaceAreaAdditionalDocument = GetPublicSurfaceAreaDocument(project);
159175

160176
if (publicSurfaceAreaAdditionalDocument == null)
161177
{
162178
continue;
163179
}
164180

165-
var sourceText = await publicSurfaceAreaAdditionalDocument.GetTextAsync(cancellationToken).ConfigureAwait(false);
181+
SourceText sourceText = await publicSurfaceAreaAdditionalDocument.GetTextAsync(cancellationToken).ConfigureAwait(false);
166182

167-
var groupedDiagnostics =
183+
IEnumerable<IGrouping<SyntaxTree, Diagnostic>> groupedDiagnostics =
168184
diagnostics
169185
.Where(d => d.Location.IsInSource)
170186
.GroupBy(d => d.Location.SourceTree);
171187

172188
var newSymbolNames = new List<string>();
189+
var symbolNamesToRemoveBuilder = ImmutableHashSet.CreateBuilder<string>();
173190

174-
foreach (var grouping in groupedDiagnostics)
191+
foreach (IGrouping<SyntaxTree, Diagnostic> grouping in groupedDiagnostics)
175192
{
176-
var document = project.GetDocument(grouping.Key);
193+
Document document = project.GetDocument(grouping.Key);
177194

178195
if (document == null)
179196
{
180197
continue;
181198
}
182199

183-
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
184-
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
200+
SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
201+
SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
185202

186-
foreach (var diagnostic in grouping)
203+
foreach (Diagnostic diagnostic in grouping)
187204
{
188205
string publicSurfaceAreaSymbolName = diagnostic.Properties[DeclarePublicAPIAnalyzer.PublicApiNamePropertyBagKey];
189206

190207
newSymbolNames.Add(publicSurfaceAreaSymbolName);
208+
209+
string siblingNamesToRemove = diagnostic.Properties[DeclarePublicAPIAnalyzer.PublicApiNamesOfSiblingsToRemovePropertyBagKey];
210+
if (siblingNamesToRemove.Length > 0)
211+
{
212+
var namesToRemove = siblingNamesToRemove.Split(DeclarePublicAPIAnalyzer.PublicApiNamesOfSiblingsToRemovePropertyBagValueSeparator.ToCharArray());
213+
foreach (var nameToRemove in namesToRemove)
214+
{
215+
symbolNamesToRemoveBuilder.Add(nameToRemove);
216+
}
217+
}
191218
}
192219
}
193220

194-
var newSourceText = AddSymbolNamesToSourceText(sourceText, newSymbolNames);
221+
var symbolNamesToRemove = symbolNamesToRemoveBuilder.ToImmutable();
222+
223+
// We shouldn't be attempting to remove any symbol name, while also adding it.
224+
Debug.Assert(newSymbolNames.All(newSymbolName => !symbolNamesToRemove.Contains(newSymbolName)));
225+
226+
SourceText newSourceText = AddSymbolNamesToSourceText(sourceText, newSymbolNames);
227+
newSourceText = RemoveSymbolNamesFromSourceText(newSourceText, symbolNamesToRemove);
195228

196229
updatedPublicSurfaceAreaText.Add(new KeyValuePair<DocumentId, SourceText>(publicSurfaceAreaAdditionalDocument.Id, newSourceText));
197230
}
198231

199-
var newSolution = this.solution;
232+
Solution newSolution = _solution;
200233

201-
foreach (var pair in updatedPublicSurfaceAreaText)
234+
foreach (KeyValuePair<DocumentId, SourceText> pair in updatedPublicSurfaceAreaText)
202235
{
203236
newSolution = newSolution.WithAdditionalDocumentText(pair.Key, pair.Value);
204237
}
@@ -217,40 +250,39 @@ public override async Task<CodeAction> GetFixAsync(FixAllContext fixAllContext)
217250

218251
switch (fixAllContext.Scope)
219252
{
220-
case FixAllScope.Document:
221-
{
222-
var diagnostics = await fixAllContext.GetDocumentDiagnosticsAsync(fixAllContext.Document).ConfigureAwait(false);
223-
diagnosticsToFix.Add(new KeyValuePair<Project, ImmutableArray<Diagnostic>>(fixAllContext.Project, diagnostics));
224-
title = string.Format(titleFormat, "document", fixAllContext.Document.Name);
225-
break;
226-
}
227-
228-
case FixAllScope.Project:
229-
{
230-
var project = fixAllContext.Project;
231-
ImmutableArray<Diagnostic> diagnostics = await fixAllContext.GetAllDiagnosticsAsync(project).ConfigureAwait(false);
232-
diagnosticsToFix.Add(new KeyValuePair<Project, ImmutableArray<Diagnostic>>(fixAllContext.Project, diagnostics));
233-
title = string.Format(titleFormat, "project", fixAllContext.Project.Name);
234-
break;
235-
}
253+
case FixAllScope.Document:
254+
{
255+
ImmutableArray<Diagnostic> diagnostics = await fixAllContext.GetDocumentDiagnosticsAsync(fixAllContext.Document).ConfigureAwait(false);
256+
diagnosticsToFix.Add(new KeyValuePair<Project, ImmutableArray<Diagnostic>>(fixAllContext.Project, diagnostics));
257+
title = string.Format(titleFormat, "document", fixAllContext.Document.Name);
258+
break;
259+
}
236260

237-
case FixAllScope.Solution:
238-
{
239-
foreach (var project in fixAllContext.Solution.Projects)
261+
case FixAllScope.Project:
240262
{
263+
Project project = fixAllContext.Project;
241264
ImmutableArray<Diagnostic> diagnostics = await fixAllContext.GetAllDiagnosticsAsync(project).ConfigureAwait(false);
242-
diagnosticsToFix.Add(new KeyValuePair<Project, ImmutableArray<Diagnostic>>(project, diagnostics));
265+
diagnosticsToFix.Add(new KeyValuePair<Project, ImmutableArray<Diagnostic>>(fixAllContext.Project, diagnostics));
266+
title = string.Format(titleFormat, "project", fixAllContext.Project.Name);
267+
break;
243268
}
244269

245-
title = "Add all items in the solution to the public API";
246-
break;
247-
}
248-
249-
case FixAllScope.Custom:
250-
return null;
270+
case FixAllScope.Solution:
271+
{
272+
foreach (Project project in fixAllContext.Solution.Projects)
273+
{
274+
ImmutableArray<Diagnostic> diagnostics = await fixAllContext.GetAllDiagnosticsAsync(project).ConfigureAwait(false);
275+
diagnosticsToFix.Add(new KeyValuePair<Project, ImmutableArray<Diagnostic>>(project, diagnostics));
276+
}
277+
278+
title = "Add all items in the solution to the public API";
279+
break;
280+
}
251281

252-
default:
253-
break;
282+
case FixAllScope.Custom:
283+
return null;
284+
default:
285+
break;
254286
}
255287

256288
return new FixAllAdditionalDocumentChangeAction(title, fixAllContext.Solution, diagnosticsToFix);

0 commit comments

Comments
 (0)