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

Commit 0897c00

Browse files
authored
Merge pull request #14 from sharwell/update-analyzer
Update analyzer
2 parents 4e3311b + 0b8d93d commit 0897c00

File tree

12 files changed

+1069
-150
lines changed

12 files changed

+1069
-150
lines changed

PublicApiAnalyzer/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
</PropertyGroup>
1919

2020
<PropertyGroup>
21-
<LangVersion>6</LangVersion>
21+
<LangVersion>7.1</LangVersion>
2222
<Features>strict</Features>
2323
</PropertyGroup>
2424

PublicApiAnalyzer/PublicApiAnalyzer.CodeFixes/ApiDesign/DeclarePublicAPIFix.cs

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ namespace PublicApiAnalyzer.ApiDesign
77
using System.Collections.Generic;
88
using System.Collections.Immutable;
99
using System.Composition;
10+
using System.Diagnostics;
1011
using System.Linq;
1112
using System.Threading;
1213
using System.Threading.Tasks;
@@ -17,7 +18,7 @@ namespace PublicApiAnalyzer.ApiDesign
1718

1819
[ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic, Name = "DeclarePublicAPIFix")]
1920
[Shared]
20-
internal class DeclarePublicAPIFix : CodeFixProvider
21+
internal sealed class DeclarePublicAPIFix : CodeFixProvider
2122
{
2223
public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } =
2324
ImmutableArray.Create(RoslynDiagnosticIds.DeclarePublicApiRuleId);
@@ -42,12 +43,14 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
4243
{
4344
string minimalSymbolName = diagnostic.Properties[DeclarePublicAPIAnalyzer.MinimalNamePropertyBagKey];
4445
string publicSurfaceAreaSymbolName = diagnostic.Properties[DeclarePublicAPIAnalyzer.PublicApiNamePropertyBagKey];
46+
ImmutableHashSet<string> siblingSymbolNamesToRemove = diagnostic.Properties[DeclarePublicAPIAnalyzer.PublicApiNamesOfSiblingsToRemovePropertyBagKey]
47+
.Split(DeclarePublicAPIAnalyzer.PublicApiNamesOfSiblingsToRemovePropertyBagValueSeparator.ToCharArray())
48+
.ToImmutableHashSet();
4549

4650
context.RegisterCodeFix(
47-
CodeAction.Create(
48-
$"Add '{minimalSymbolName}' to public API",
49-
c => this.GetFixAsync(publicSurfaceAreaDocument, publicSurfaceAreaSymbolName, c),
50-
nameof(DeclarePublicAPIFix)),
51+
new AdditionalDocumentChangeAction(
52+
$"Add {minimalSymbolName} to public API",
53+
c => this.GetFixAsync(publicSurfaceAreaDocument, publicSurfaceAreaSymbolName, siblingSymbolNamesToRemove, c)),
5154
diagnostic);
5255
}
5356
}
@@ -61,14 +64,30 @@ private static SourceText AddSymbolNamesToSourceText(SourceText sourceText, IEnu
6164
{
6265
HashSet<string> lines = GetLinesFromSourceText(sourceText);
6366

64-
foreach (var name in newSymbolNames)
67+
foreach (string name in newSymbolNames)
6568
{
6669
lines.Add(name);
6770
}
6871

6972
var sortedLines = lines.OrderBy(s => s, StringComparer.Ordinal);
7073

71-
var newSourceText = sourceText.Replace(new TextSpan(0, sourceText.Length), string.Join(Environment.NewLine, sortedLines));
74+
var newSourceText = sourceText.Replace(new TextSpan(0, sourceText.Length), string.Join(Environment.NewLine, sortedLines) + GetEndOfFileText(sourceText));
75+
return newSourceText;
76+
}
77+
78+
private static SourceText RemoveSymbolNamesFromSourceText(SourceText sourceText, ImmutableHashSet<string> linesToRemove)
79+
{
80+
if (linesToRemove.IsEmpty)
81+
{
82+
return sourceText;
83+
}
84+
85+
var lines = GetLinesFromSourceText(sourceText);
86+
var newLines = lines.Where(line => !linesToRemove.Contains(line));
87+
88+
var sortedLines = newLines.OrderBy(s => s, StringComparer.Ordinal);
89+
90+
var newSourceText = sourceText.Replace(new TextSpan(0, sourceText.Length), string.Join(Environment.NewLine, sortedLines) + GetEndOfFileText(sourceText));
7291
return newSourceText;
7392
}
7493

@@ -78,7 +97,7 @@ private static HashSet<string> GetLinesFromSourceText(SourceText sourceText)
7897

7998
foreach (var textLine in sourceText.Lines)
8099
{
81-
var text = textLine.ToString();
100+
string text = textLine.ToString();
82101
if (!string.IsNullOrWhiteSpace(text))
83102
{
84103
lines.Add(text);
@@ -88,28 +107,28 @@ private static HashSet<string> GetLinesFromSourceText(SourceText sourceText)
88107
return lines;
89108
}
90109

91-
private static ISymbol FindDeclaration(SyntaxNode root, Location location, SemanticModel semanticModel, CancellationToken cancellationToken)
110+
/// <summary>
111+
/// Returns the trailing newline from the end of <paramref name="sourceText"/>, if one exists.
112+
/// </summary>
113+
/// <param name="sourceText">The source text.</param>
114+
/// <returns><see cref="Environment.NewLine"/> if <paramref name="sourceText"/> ends with a trailing newline;
115+
/// otherwise, <see cref="string.Empty"/>.</returns>
116+
private static string GetEndOfFileText(SourceText sourceText)
92117
{
93-
var node = root.FindNode(location.SourceSpan);
94-
ISymbol symbol = null;
95-
while (node != null)
118+
if (sourceText.Length == 0)
96119
{
97-
symbol = semanticModel.GetDeclaredSymbol(node, cancellationToken);
98-
if (symbol != null)
99-
{
100-
break;
101-
}
102-
103-
node = node.Parent;
120+
return string.Empty;
104121
}
105122

106-
return symbol;
123+
var lastLine = sourceText.Lines[sourceText.Lines.Count - 1];
124+
return lastLine.Span.IsEmpty ? Environment.NewLine : string.Empty;
107125
}
108126

109-
private async Task<Solution> GetFixAsync(TextDocument publicSurfaceAreaDocument, string newSymbolName, CancellationToken cancellationToken)
127+
private async Task<Solution> GetFixAsync(TextDocument publicSurfaceAreaDocument, string newSymbolName, ImmutableHashSet<string> siblingSymbolNamesToRemove, CancellationToken cancellationToken)
110128
{
111129
var sourceText = await publicSurfaceAreaDocument.GetTextAsync(cancellationToken).ConfigureAwait(false);
112130
var newSourceText = AddSymbolNamesToSourceText(sourceText, new[] { newSymbolName });
131+
newSourceText = RemoveSymbolNamesFromSourceText(newSourceText, siblingSymbolNamesToRemove);
113132

114133
return publicSurfaceAreaDocument.Project.Solution.WithAdditionalDocumentText(publicSurfaceAreaDocument.Id, newSourceText);
115134
}
@@ -126,6 +145,8 @@ public AdditionalDocumentChangeAction(string title, Func<CancellationToken, Task
126145

127146
public override string Title { get; }
128147

148+
public override string EquivalenceKey => this.Title;
149+
129150
protected override Task<Solution> GetChangedSolutionAsync(CancellationToken cancellationToken)
130151
{
131152
return this.createChangedAdditionalDocument(cancellationToken);
@@ -170,6 +191,7 @@ protected override async Task<Solution> GetChangedSolutionAsync(CancellationToke
170191
.GroupBy(d => d.Location.SourceTree);
171192

172193
var newSymbolNames = new List<string>();
194+
var symbolNamesToRemoveBuilder = ImmutableHashSet.CreateBuilder<string>();
173195

174196
foreach (var grouping in groupedDiagnostics)
175197
{
@@ -188,10 +210,26 @@ protected override async Task<Solution> GetChangedSolutionAsync(CancellationToke
188210
string publicSurfaceAreaSymbolName = diagnostic.Properties[DeclarePublicAPIAnalyzer.PublicApiNamePropertyBagKey];
189211

190212
newSymbolNames.Add(publicSurfaceAreaSymbolName);
213+
214+
string siblingNamesToRemove = diagnostic.Properties[DeclarePublicAPIAnalyzer.PublicApiNamesOfSiblingsToRemovePropertyBagKey];
215+
if (siblingNamesToRemove.Length > 0)
216+
{
217+
var namesToRemove = siblingNamesToRemove.Split(DeclarePublicAPIAnalyzer.PublicApiNamesOfSiblingsToRemovePropertyBagValueSeparator.ToCharArray());
218+
foreach (var nameToRemove in namesToRemove)
219+
{
220+
symbolNamesToRemoveBuilder.Add(nameToRemove);
221+
}
222+
}
191223
}
192224
}
193225

226+
var symbolNamesToRemove = symbolNamesToRemoveBuilder.ToImmutable();
227+
228+
// We shouldn't be attempting to remove any symbol name, while also adding it.
229+
Debug.Assert(newSymbolNames.All(newSymbolName => !symbolNamesToRemove.Contains(newSymbolName)), "Assertion failed: newSymbolNames.All(newSymbolName => !symbolNamesToRemove.Contains(newSymbolName))");
230+
194231
var newSourceText = AddSymbolNamesToSourceText(sourceText, newSymbolNames);
232+
newSourceText = RemoveSymbolNamesFromSourceText(newSourceText, symbolNamesToRemove);
195233

196234
updatedPublicSurfaceAreaText.Add(new KeyValuePair<DocumentId, SourceText>(publicSurfaceAreaAdditionalDocument.Id, newSourceText));
197235
}
@@ -228,7 +266,7 @@ public override async Task<CodeAction> GetFixAsync(FixAllContext fixAllContext)
228266
case FixAllScope.Project:
229267
{
230268
var project = fixAllContext.Project;
231-
ImmutableArray<Diagnostic> diagnostics = await fixAllContext.GetAllDiagnosticsAsync(project).ConfigureAwait(false);
269+
var diagnostics = await fixAllContext.GetAllDiagnosticsAsync(project).ConfigureAwait(false);
232270
diagnosticsToFix.Add(new KeyValuePair<Project, ImmutableArray<Diagnostic>>(fixAllContext.Project, diagnostics));
233271
title = string.Format(titleFormat, "project", fixAllContext.Project.Name);
234272
break;
@@ -238,7 +276,7 @@ public override async Task<CodeAction> GetFixAsync(FixAllContext fixAllContext)
238276
{
239277
foreach (var project in fixAllContext.Solution.Projects)
240278
{
241-
ImmutableArray<Diagnostic> diagnostics = await fixAllContext.GetAllDiagnosticsAsync(project).ConfigureAwait(false);
279+
var diagnostics = await fixAllContext.GetAllDiagnosticsAsync(project).ConfigureAwait(false);
242280
diagnosticsToFix.Add(new KeyValuePair<Project, ImmutableArray<Diagnostic>>(project, diagnostics));
243281
}
244282

0 commit comments

Comments
 (0)