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