@@ -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