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
5
17
{
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
21
20
{
22
- public sealed override ImmutableArray < string > FixableDiagnosticIds { get ; } =
23
- ImmutableArray . Create ( RoslynDiagnosticIds . DeclarePublicApiRuleId ) ;
21
+ public sealed override ImmutableArray < string > FixableDiagnosticIds => ImmutableArray . Create ( RoslynDiagnosticIds . DeclarePublicApiRuleId ) ;
24
22
25
23
public sealed override FixAllProvider GetFixAllProvider ( )
26
24
{
@@ -29,26 +27,28 @@ public sealed override FixAllProvider GetFixAllProvider()
29
27
30
28
public sealed override async Task RegisterCodeFixesAsync ( CodeFixContext context )
31
29
{
32
- var project = context . Document . Project ;
30
+ Project project = context . Document . Project ;
33
31
TextDocument publicSurfaceAreaDocument = GetPublicSurfaceAreaDocument ( project ) ;
34
32
if ( publicSurfaceAreaDocument == null )
35
33
{
36
34
return ;
37
35
}
38
36
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 )
42
40
{
43
41
string minimalSymbolName = diagnostic . Properties [ DeclarePublicAPIAnalyzer . MinimalNamePropertyBagKey ] ;
44
42
string publicSurfaceAreaSymbolName = diagnostic . Properties [ DeclarePublicAPIAnalyzer . PublicApiNamePropertyBagKey ] ;
43
+ ImmutableHashSet < string > siblingSymbolNamesToRemove = diagnostic . Properties [ DeclarePublicAPIAnalyzer . PublicApiNamesOfSiblingsToRemovePropertyBagKey ]
44
+ . Split ( DeclarePublicAPIAnalyzer . PublicApiNamesOfSiblingsToRemovePropertyBagValueSeparator . ToCharArray ( ) )
45
+ . ToImmutableHashSet ( ) ;
45
46
46
47
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 ) ;
52
52
}
53
53
}
54
54
@@ -57,91 +57,107 @@ private static TextDocument GetPublicSurfaceAreaDocument(Project project)
57
57
return project . AdditionalDocuments . FirstOrDefault ( doc => doc . Name . Equals ( DeclarePublicAPIAnalyzer . UnshippedFileName , StringComparison . Ordinal ) ) ;
58
58
}
59
59
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
+
60
69
private static SourceText AddSymbolNamesToSourceText ( SourceText sourceText , IEnumerable < string > newSymbolNames )
61
70
{
62
71
HashSet < string > lines = GetLinesFromSourceText ( sourceText ) ;
63
72
64
- foreach ( var name in newSymbolNames )
73
+ foreach ( string name in newSymbolNames )
65
74
{
66
75
lines . Add ( name ) ;
67
76
}
68
77
69
- var sortedLines = lines . OrderBy ( s => s , StringComparer . Ordinal ) ;
78
+ IOrderedEnumerable < string > sortedLines = lines . OrderBy ( s => s , StringComparer . Ordinal ) ;
70
79
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 ) ) ;
72
81
return newSourceText ;
73
82
}
74
83
75
- private static HashSet < string > GetLinesFromSourceText ( SourceText sourceText )
84
+ private static SourceText RemoveSymbolNamesFromSourceText ( SourceText sourceText , ImmutableHashSet < string > linesToRemove )
76
85
{
77
- var lines = new HashSet < string > ( ) ;
78
-
79
- foreach ( var textLine in sourceText . Lines )
86
+ if ( linesToRemove . IsEmpty )
80
87
{
81
- var text = textLine . ToString ( ) ;
82
- if ( ! string . IsNullOrWhiteSpace ( text ) )
83
- {
84
- lines . Add ( text ) ;
85
- }
88
+ return sourceText ;
86
89
}
87
90
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 ;
89
98
}
90
99
91
- private static ISymbol FindDeclaration ( SyntaxNode root , Location location , SemanticModel semanticModel , CancellationToken cancellationToken )
100
+ private static HashSet < string > GetLinesFromSourceText ( SourceText sourceText )
92
101
{
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 )
96
105
{
97
- symbol = semanticModel . GetDeclaredSymbol ( node , cancellationToken ) ;
98
- if ( symbol != null )
106
+ string text = textLine . ToString ( ) ;
107
+ if ( ! string . IsNullOrWhiteSpace ( text ) )
99
108
{
100
- break ;
109
+ lines . Add ( text ) ;
101
110
}
102
-
103
- node = node . Parent ;
104
111
}
105
112
106
- return symbol ;
113
+ return lines ;
107
114
}
108
115
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 )
110
123
{
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 ;
113
126
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 ;
115
129
}
116
130
117
131
private class AdditionalDocumentChangeAction : CodeAction
118
132
{
119
- private readonly Func < CancellationToken , Task < Solution > > createChangedAdditionalDocument ;
133
+ private readonly Func < CancellationToken , Task < Solution > > _createChangedAdditionalDocument ;
120
134
121
135
public AdditionalDocumentChangeAction ( string title , Func < CancellationToken , Task < Solution > > createChangedAdditionalDocument )
122
136
{
123
137
this . Title = title ;
124
- this . createChangedAdditionalDocument = createChangedAdditionalDocument ;
138
+ _createChangedAdditionalDocument = createChangedAdditionalDocument ;
125
139
}
126
140
127
141
public override string Title { get ; }
128
142
143
+ public override string EquivalenceKey => Title ;
144
+
129
145
protected override Task < Solution > GetChangedSolutionAsync ( CancellationToken cancellationToken )
130
146
{
131
- return this . createChangedAdditionalDocument ( cancellationToken ) ;
147
+ return _createChangedAdditionalDocument ( cancellationToken ) ;
132
148
}
133
149
}
134
150
135
151
private class FixAllAdditionalDocumentChangeAction : CodeAction
136
152
{
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 ;
139
155
140
156
public FixAllAdditionalDocumentChangeAction ( string title , Solution solution , List < KeyValuePair < Project , ImmutableArray < Diagnostic > > > diagnosticsToFix )
141
157
{
142
158
this . Title = title ;
143
- this . solution = solution ;
144
- this . diagnosticsToFix = diagnosticsToFix ;
159
+ _solution = solution ;
160
+ _diagnosticsToFix = diagnosticsToFix ;
145
161
}
146
162
147
163
public override string Title { get ; }
@@ -150,55 +166,72 @@ protected override async Task<Solution> GetChangedSolutionAsync(CancellationToke
150
166
{
151
167
var updatedPublicSurfaceAreaText = new List < KeyValuePair < DocumentId , SourceText > > ( ) ;
152
168
153
- foreach ( var pair in this . diagnosticsToFix )
169
+ foreach ( KeyValuePair < Project , ImmutableArray < Diagnostic > > pair in _diagnosticsToFix )
154
170
{
155
- var project = pair . Key ;
156
- var diagnostics = pair . Value ;
171
+ Project project = pair . Key ;
172
+ ImmutableArray < Diagnostic > diagnostics = pair . Value ;
157
173
158
- var publicSurfaceAreaAdditionalDocument = GetPublicSurfaceAreaDocument ( project ) ;
174
+ TextDocument publicSurfaceAreaAdditionalDocument = GetPublicSurfaceAreaDocument ( project ) ;
159
175
160
176
if ( publicSurfaceAreaAdditionalDocument == null )
161
177
{
162
178
continue ;
163
179
}
164
180
165
- var sourceText = await publicSurfaceAreaAdditionalDocument . GetTextAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
181
+ SourceText sourceText = await publicSurfaceAreaAdditionalDocument . GetTextAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
166
182
167
- var groupedDiagnostics =
183
+ IEnumerable < IGrouping < SyntaxTree , Diagnostic > > groupedDiagnostics =
168
184
diagnostics
169
185
. Where ( d => d . Location . IsInSource )
170
186
. GroupBy ( d => d . Location . SourceTree ) ;
171
187
172
188
var newSymbolNames = new List < string > ( ) ;
189
+ var symbolNamesToRemoveBuilder = ImmutableHashSet . CreateBuilder < string > ( ) ;
173
190
174
- foreach ( var grouping in groupedDiagnostics )
191
+ foreach ( IGrouping < SyntaxTree , Diagnostic > grouping in groupedDiagnostics )
175
192
{
176
- var document = project . GetDocument ( grouping . Key ) ;
193
+ Document document = project . GetDocument ( grouping . Key ) ;
177
194
178
195
if ( document == null )
179
196
{
180
197
continue ;
181
198
}
182
199
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 ) ;
185
202
186
- foreach ( var diagnostic in grouping )
203
+ foreach ( Diagnostic diagnostic in grouping )
187
204
{
188
205
string publicSurfaceAreaSymbolName = diagnostic . Properties [ DeclarePublicAPIAnalyzer . PublicApiNamePropertyBagKey ] ;
189
206
190
207
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
+ }
191
218
}
192
219
}
193
220
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 ) ;
195
228
196
229
updatedPublicSurfaceAreaText . Add ( new KeyValuePair < DocumentId , SourceText > ( publicSurfaceAreaAdditionalDocument . Id , newSourceText ) ) ;
197
230
}
198
231
199
- var newSolution = this . solution ;
232
+ Solution newSolution = _solution ;
200
233
201
- foreach ( var pair in updatedPublicSurfaceAreaText )
234
+ foreach ( KeyValuePair < DocumentId , SourceText > pair in updatedPublicSurfaceAreaText )
202
235
{
203
236
newSolution = newSolution . WithAdditionalDocumentText ( pair . Key , pair . Value ) ;
204
237
}
@@ -217,40 +250,39 @@ public override async Task<CodeAction> GetFixAsync(FixAllContext fixAllContext)
217
250
218
251
switch ( fixAllContext . Scope )
219
252
{
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
+ }
236
260
237
- case FixAllScope . Solution :
238
- {
239
- foreach ( var project in fixAllContext . Solution . Projects )
261
+ case FixAllScope . Project :
240
262
{
263
+ Project project = fixAllContext . Project ;
241
264
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 ;
243
268
}
244
269
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
+ }
251
281
252
- default :
253
- break ;
282
+ case FixAllScope . Custom :
283
+ return null ;
284
+ default :
285
+ break ;
254
286
}
255
287
256
288
return new FixAllAdditionalDocumentChangeAction ( title , fixAllContext . Solution , diagnosticsToFix ) ;
0 commit comments