@@ -7,6 +7,7 @@ namespace PublicApiAnalyzer.ApiDesign
7
7
using System . Collections . Generic ;
8
8
using System . Collections . Immutable ;
9
9
using System . Composition ;
10
+ using System . Diagnostics ;
10
11
using System . Linq ;
11
12
using System . Threading ;
12
13
using System . Threading . Tasks ;
@@ -17,7 +18,7 @@ namespace PublicApiAnalyzer.ApiDesign
17
18
18
19
[ ExportCodeFixProvider ( LanguageNames . CSharp , LanguageNames . VisualBasic , Name = "DeclarePublicAPIFix" ) ]
19
20
[ Shared ]
20
- internal class DeclarePublicAPIFix : CodeFixProvider
21
+ internal sealed class DeclarePublicAPIFix : CodeFixProvider
21
22
{
22
23
public sealed override ImmutableArray < string > FixableDiagnosticIds { get ; } =
23
24
ImmutableArray . Create ( RoslynDiagnosticIds . DeclarePublicApiRuleId ) ;
@@ -42,12 +43,14 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
42
43
{
43
44
string minimalSymbolName = diagnostic . Properties [ DeclarePublicAPIAnalyzer . MinimalNamePropertyBagKey ] ;
44
45
string publicSurfaceAreaSymbolName = diagnostic . Properties [ DeclarePublicAPIAnalyzer . PublicApiNamePropertyBagKey ] ;
46
+ ImmutableHashSet < string > siblingSymbolNamesToRemove = diagnostic . Properties [ DeclarePublicAPIAnalyzer . PublicApiNamesOfSiblingsToRemovePropertyBagKey ]
47
+ . Split ( DeclarePublicAPIAnalyzer . PublicApiNamesOfSiblingsToRemovePropertyBagValueSeparator . ToCharArray ( ) )
48
+ . ToImmutableHashSet ( ) ;
45
49
46
50
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 ) ) ,
51
54
diagnostic ) ;
52
55
}
53
56
}
@@ -61,14 +64,30 @@ private static SourceText AddSymbolNamesToSourceText(SourceText sourceText, IEnu
61
64
{
62
65
HashSet < string > lines = GetLinesFromSourceText ( sourceText ) ;
63
66
64
- foreach ( var name in newSymbolNames )
67
+ foreach ( string name in newSymbolNames )
65
68
{
66
69
lines . Add ( name ) ;
67
70
}
68
71
69
72
var sortedLines = lines . OrderBy ( s => s , StringComparer . Ordinal ) ;
70
73
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 ) ) ;
72
91
return newSourceText ;
73
92
}
74
93
@@ -78,7 +97,7 @@ private static HashSet<string> GetLinesFromSourceText(SourceText sourceText)
78
97
79
98
foreach ( var textLine in sourceText . Lines )
80
99
{
81
- var text = textLine . ToString ( ) ;
100
+ string text = textLine . ToString ( ) ;
82
101
if ( ! string . IsNullOrWhiteSpace ( text ) )
83
102
{
84
103
lines . Add ( text ) ;
@@ -88,28 +107,28 @@ private static HashSet<string> GetLinesFromSourceText(SourceText sourceText)
88
107
return lines ;
89
108
}
90
109
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 )
92
117
{
93
- var node = root . FindNode ( location . SourceSpan ) ;
94
- ISymbol symbol = null ;
95
- while ( node != null )
118
+ if ( sourceText . Length == 0 )
96
119
{
97
- symbol = semanticModel . GetDeclaredSymbol ( node , cancellationToken ) ;
98
- if ( symbol != null )
99
- {
100
- break ;
101
- }
102
-
103
- node = node . Parent ;
120
+ return string . Empty ;
104
121
}
105
122
106
- return symbol ;
123
+ var lastLine = sourceText . Lines [ sourceText . Lines . Count - 1 ] ;
124
+ return lastLine . Span . IsEmpty ? Environment . NewLine : string . Empty ;
107
125
}
108
126
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 )
110
128
{
111
129
var sourceText = await publicSurfaceAreaDocument . GetTextAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
112
130
var newSourceText = AddSymbolNamesToSourceText ( sourceText , new [ ] { newSymbolName } ) ;
131
+ newSourceText = RemoveSymbolNamesFromSourceText ( newSourceText , siblingSymbolNamesToRemove ) ;
113
132
114
133
return publicSurfaceAreaDocument . Project . Solution . WithAdditionalDocumentText ( publicSurfaceAreaDocument . Id , newSourceText ) ;
115
134
}
@@ -126,6 +145,8 @@ public AdditionalDocumentChangeAction(string title, Func<CancellationToken, Task
126
145
127
146
public override string Title { get ; }
128
147
148
+ public override string EquivalenceKey => this . Title ;
149
+
129
150
protected override Task < Solution > GetChangedSolutionAsync ( CancellationToken cancellationToken )
130
151
{
131
152
return this . createChangedAdditionalDocument ( cancellationToken ) ;
@@ -170,6 +191,7 @@ protected override async Task<Solution> GetChangedSolutionAsync(CancellationToke
170
191
. GroupBy ( d => d . Location . SourceTree ) ;
171
192
172
193
var newSymbolNames = new List < string > ( ) ;
194
+ var symbolNamesToRemoveBuilder = ImmutableHashSet . CreateBuilder < string > ( ) ;
173
195
174
196
foreach ( var grouping in groupedDiagnostics )
175
197
{
@@ -188,10 +210,26 @@ protected override async Task<Solution> GetChangedSolutionAsync(CancellationToke
188
210
string publicSurfaceAreaSymbolName = diagnostic . Properties [ DeclarePublicAPIAnalyzer . PublicApiNamePropertyBagKey ] ;
189
211
190
212
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
+ }
191
223
}
192
224
}
193
225
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
+
194
231
var newSourceText = AddSymbolNamesToSourceText ( sourceText , newSymbolNames ) ;
232
+ newSourceText = RemoveSymbolNamesFromSourceText ( newSourceText , symbolNamesToRemove ) ;
195
233
196
234
updatedPublicSurfaceAreaText . Add ( new KeyValuePair < DocumentId , SourceText > ( publicSurfaceAreaAdditionalDocument . Id , newSourceText ) ) ;
197
235
}
@@ -228,7 +266,7 @@ public override async Task<CodeAction> GetFixAsync(FixAllContext fixAllContext)
228
266
case FixAllScope . Project :
229
267
{
230
268
var project = fixAllContext . Project ;
231
- ImmutableArray < Diagnostic > diagnostics = await fixAllContext . GetAllDiagnosticsAsync ( project ) . ConfigureAwait ( false ) ;
269
+ var diagnostics = await fixAllContext . GetAllDiagnosticsAsync ( project ) . ConfigureAwait ( false ) ;
232
270
diagnosticsToFix . Add ( new KeyValuePair < Project , ImmutableArray < Diagnostic > > ( fixAllContext . Project , diagnostics ) ) ;
233
271
title = string . Format ( titleFormat , "project" , fixAllContext . Project . Name ) ;
234
272
break ;
@@ -238,7 +276,7 @@ public override async Task<CodeAction> GetFixAsync(FixAllContext fixAllContext)
238
276
{
239
277
foreach ( var project in fixAllContext . Solution . Projects )
240
278
{
241
- ImmutableArray < Diagnostic > diagnostics = await fixAllContext . GetAllDiagnosticsAsync ( project ) . ConfigureAwait ( false ) ;
279
+ var diagnostics = await fixAllContext . GetAllDiagnosticsAsync ( project ) . ConfigureAwait ( false ) ;
242
280
diagnosticsToFix . Add ( new KeyValuePair < Project , ImmutableArray < Diagnostic > > ( project , diagnostics ) ) ;
243
281
}
244
282
0 commit comments