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

Commit 4781562

Browse files
committed
Merge pull request #2 from sharwell/initial-impl
Add initial implementation from roslyn-analyzers
2 parents f8eb9fd + 8030bca commit 4781562

15 files changed

+1486
-31
lines changed
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
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
5+
{
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
21+
{
22+
public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } =
23+
ImmutableArray.Create(RoslynDiagnosticIds.DeclarePublicApiRuleId);
24+
25+
public sealed override FixAllProvider GetFixAllProvider()
26+
{
27+
return new PublicSurfaceAreaFixAllProvider();
28+
}
29+
30+
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
31+
{
32+
var project = context.Document.Project;
33+
TextDocument publicSurfaceAreaDocument = GetPublicSurfaceAreaDocument(project);
34+
if (publicSurfaceAreaDocument == null)
35+
{
36+
return;
37+
}
38+
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)
42+
{
43+
string minimalSymbolName = diagnostic.Properties[DeclarePublicAPIAnalyzer.MinimalNamePropertyBagKey];
44+
string publicSurfaceAreaSymbolName = diagnostic.Properties[DeclarePublicAPIAnalyzer.PublicApiNamePropertyBagKey];
45+
46+
context.RegisterCodeFix(
47+
CodeAction.Create(
48+
$"Add '{minimalSymbolName}' to public API",
49+
c => this.GetFixAsync(publicSurfaceAreaDocument, publicSurfaceAreaSymbolName, c),
50+
nameof(DeclarePublicAPIFix)),
51+
diagnostic);
52+
}
53+
}
54+
55+
private static TextDocument GetPublicSurfaceAreaDocument(Project project)
56+
{
57+
return project.AdditionalDocuments.FirstOrDefault(doc => doc.Name.Equals(DeclarePublicAPIAnalyzer.UnshippedFileName, StringComparison.Ordinal));
58+
}
59+
60+
private static SourceText AddSymbolNamesToSourceText(SourceText sourceText, IEnumerable<string> newSymbolNames)
61+
{
62+
HashSet<string> lines = GetLinesFromSourceText(sourceText);
63+
64+
foreach (var name in newSymbolNames)
65+
{
66+
lines.Add(name);
67+
}
68+
69+
var sortedLines = lines.OrderBy(s => s, StringComparer.Ordinal);
70+
71+
var newSourceText = sourceText.Replace(new TextSpan(0, sourceText.Length), string.Join(Environment.NewLine, sortedLines));
72+
return newSourceText;
73+
}
74+
75+
private static HashSet<string> GetLinesFromSourceText(SourceText sourceText)
76+
{
77+
var lines = new HashSet<string>();
78+
79+
foreach (var textLine in sourceText.Lines)
80+
{
81+
var text = textLine.ToString();
82+
if (!string.IsNullOrWhiteSpace(text))
83+
{
84+
lines.Add(text);
85+
}
86+
}
87+
88+
return lines;
89+
}
90+
91+
private static ISymbol FindDeclaration(SyntaxNode root, Location location, SemanticModel semanticModel, CancellationToken cancellationToken)
92+
{
93+
var node = root.FindNode(location.SourceSpan);
94+
ISymbol symbol = null;
95+
while (node != null)
96+
{
97+
symbol = semanticModel.GetDeclaredSymbol(node, cancellationToken);
98+
if (symbol != null)
99+
{
100+
break;
101+
}
102+
103+
node = node.Parent;
104+
}
105+
106+
return symbol;
107+
}
108+
109+
private async Task<Solution> GetFixAsync(TextDocument publicSurfaceAreaDocument, string newSymbolName, CancellationToken cancellationToken)
110+
{
111+
var sourceText = await publicSurfaceAreaDocument.GetTextAsync(cancellationToken).ConfigureAwait(false);
112+
var newSourceText = AddSymbolNamesToSourceText(sourceText, new[] { newSymbolName });
113+
114+
return publicSurfaceAreaDocument.Project.Solution.WithAdditionalDocumentText(publicSurfaceAreaDocument.Id, newSourceText);
115+
}
116+
117+
private class AdditionalDocumentChangeAction : CodeAction
118+
{
119+
private readonly Func<CancellationToken, Task<Solution>> createChangedAdditionalDocument;
120+
121+
public AdditionalDocumentChangeAction(string title, Func<CancellationToken, Task<Solution>> createChangedAdditionalDocument)
122+
{
123+
this.Title = title;
124+
this.createChangedAdditionalDocument = createChangedAdditionalDocument;
125+
}
126+
127+
public override string Title { get; }
128+
129+
protected override Task<Solution> GetChangedSolutionAsync(CancellationToken cancellationToken)
130+
{
131+
return this.createChangedAdditionalDocument(cancellationToken);
132+
}
133+
}
134+
135+
private class FixAllAdditionalDocumentChangeAction : CodeAction
136+
{
137+
private readonly List<KeyValuePair<Project, ImmutableArray<Diagnostic>>> diagnosticsToFix;
138+
private readonly Solution solution;
139+
140+
public FixAllAdditionalDocumentChangeAction(string title, Solution solution, List<KeyValuePair<Project, ImmutableArray<Diagnostic>>> diagnosticsToFix)
141+
{
142+
this.Title = title;
143+
this.solution = solution;
144+
this.diagnosticsToFix = diagnosticsToFix;
145+
}
146+
147+
public override string Title { get; }
148+
149+
protected override async Task<Solution> GetChangedSolutionAsync(CancellationToken cancellationToken)
150+
{
151+
var updatedPublicSurfaceAreaText = new List<KeyValuePair<DocumentId, SourceText>>();
152+
153+
foreach (var pair in this.diagnosticsToFix)
154+
{
155+
var project = pair.Key;
156+
var diagnostics = pair.Value;
157+
158+
var publicSurfaceAreaAdditionalDocument = GetPublicSurfaceAreaDocument(project);
159+
160+
if (publicSurfaceAreaAdditionalDocument == null)
161+
{
162+
continue;
163+
}
164+
165+
var sourceText = await publicSurfaceAreaAdditionalDocument.GetTextAsync(cancellationToken).ConfigureAwait(false);
166+
167+
var groupedDiagnostics =
168+
diagnostics
169+
.Where(d => d.Location.IsInSource)
170+
.GroupBy(d => d.Location.SourceTree);
171+
172+
var newSymbolNames = new List<string>();
173+
174+
foreach (var grouping in groupedDiagnostics)
175+
{
176+
var document = project.GetDocument(grouping.Key);
177+
178+
if (document == null)
179+
{
180+
continue;
181+
}
182+
183+
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
184+
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
185+
186+
foreach (var diagnostic in grouping)
187+
{
188+
string publicSurfaceAreaSymbolName = diagnostic.Properties[DeclarePublicAPIAnalyzer.PublicApiNamePropertyBagKey];
189+
190+
newSymbolNames.Add(publicSurfaceAreaSymbolName);
191+
}
192+
}
193+
194+
var newSourceText = AddSymbolNamesToSourceText(sourceText, newSymbolNames);
195+
196+
updatedPublicSurfaceAreaText.Add(new KeyValuePair<DocumentId, SourceText>(publicSurfaceAreaAdditionalDocument.Id, newSourceText));
197+
}
198+
199+
var newSolution = this.solution;
200+
201+
foreach (var pair in updatedPublicSurfaceAreaText)
202+
{
203+
newSolution = newSolution.WithAdditionalDocumentText(pair.Key, pair.Value);
204+
}
205+
206+
return newSolution;
207+
}
208+
}
209+
210+
private class PublicSurfaceAreaFixAllProvider : FixAllProvider
211+
{
212+
public override async Task<CodeAction> GetFixAsync(FixAllContext fixAllContext)
213+
{
214+
var diagnosticsToFix = new List<KeyValuePair<Project, ImmutableArray<Diagnostic>>>();
215+
string titleFormat = "Add all items in {0} {1} to the public API";
216+
string title = null;
217+
218+
switch (fixAllContext.Scope)
219+
{
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+
}
236+
237+
case FixAllScope.Solution:
238+
{
239+
foreach (var project in fixAllContext.Solution.Projects)
240+
{
241+
ImmutableArray<Diagnostic> diagnostics = await fixAllContext.GetAllDiagnosticsAsync(project).ConfigureAwait(false);
242+
diagnosticsToFix.Add(new KeyValuePair<Project, ImmutableArray<Diagnostic>>(project, diagnostics));
243+
}
244+
245+
title = "Add all items in the solution to the public API";
246+
break;
247+
}
248+
249+
case FixAllScope.Custom:
250+
return null;
251+
252+
default:
253+
break;
254+
}
255+
256+
return new FixAllAdditionalDocumentChangeAction(title, fixAllContext.Solution, diagnosticsToFix);
257+
}
258+
}
259+
}
260+
}

PublicApiAnalyzer/PublicApiAnalyzer.CodeFixes/PublicApiAnalyzer.CodeFixes.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
<DefineConstants>$(DefineConstants);DEVELOPMENT_KEY</DefineConstants>
4848
</PropertyGroup>
4949
<ItemGroup>
50+
<Compile Include="ApiDesign\DeclarePublicAPIFix.cs" />
5051
<Compile Include="Helpers\CustomBatchFixAllProvider.cs" />
5152
<Compile Include="Helpers\CustomFixAllProviders.cs" />
5253
<Compile Include="Helpers\DocumentBasedFixAllProvider.cs" />

0 commit comments

Comments
 (0)