22// The .NET Foundation licenses this file to you under the MIT license.
33// See the LICENSE file in the project root for more information.
44
5+ using System . Collections . Generic ;
56using System . Collections . Immutable ;
67using System . Composition ;
78using System . Diagnostics . CodeAnalysis ;
@@ -217,6 +218,8 @@ private static async Task<Document> ConvertToPartialProperty(
217218 syntaxEditor ,
218219 defaultValueExpression ) ;
219220
221+ RemoveLeftoverLeadingEndOfLines ( [ fieldDeclaration ] , syntaxEditor ) ;
222+
220223 // Create the new document with the single change
221224 return document . WithSyntaxRoot ( syntaxEditor . GetChangedRoot ( ) ) ;
222225 }
@@ -305,39 +308,6 @@ private static void ConvertToPartialProperty(
305308 ] ) ) . WithTrailingTrivia ( propertyDeclaration . AccessorList . GetTrailingTrivia ( ) ) ) ;
306309 } ) ;
307310
308- // Special handling for the leading trivia of members following the field declaration we are about to remove.
309- // There is an edge case that can happen when a type declaration is as follows:
310- //
311- // class ContainingType
312- // {
313- // public static readonly DependencyProperty NameProperty = ...;
314- //
315- // public void SomeOtherMember() { }
316- //
317- // public string? Name { ... }
318- // }
319- //
320- // In this case, just removing the target field for the dependency property being rewritten (that is, 'NameProperty')
321- // will cause an extra blank line to be left after the edits, right above the member immediately following the field.
322- // To work around this, we look for such a member and check its trivia, and then manually remove a leading blank line.
323- if ( fieldDeclaration . Parent is TypeDeclarationSyntax fieldParentTypeDeclaration )
324- {
325- int fieldDeclarationIndex = fieldParentTypeDeclaration . Members . IndexOf ( fieldDeclaration ) ;
326-
327- // Check whether there is a member immediatley following the field
328- if ( fieldDeclarationIndex >= 0 && fieldDeclarationIndex < fieldParentTypeDeclaration . Members . Count - 1 )
329- {
330- MemberDeclarationSyntax nextMember = fieldParentTypeDeclaration . Members [ fieldDeclarationIndex + 1 ] ;
331- SyntaxTriviaList leadingTrivia = nextMember . GetLeadingTrivia ( ) ;
332-
333- // Check whether this member has a first leading trivia that's just a blank line: we want to remove this one
334- if ( leadingTrivia . Count > 0 && leadingTrivia [ 0 ] . IsKind ( SyntaxKind . EndOfLineTrivia ) )
335- {
336- syntaxEditor . ReplaceNode ( nextMember , ( nextMember , _ ) => nextMember . WithLeadingTrivia ( leadingTrivia . RemoveAt ( 0 ) ) ) ;
337- }
338- }
339- }
340-
341311 // Also remove the field declaration (it'll be generated now)
342312 syntaxEditor . RemoveNode ( fieldDeclaration ) ;
343313
@@ -352,6 +322,58 @@ private static void ConvertToPartialProperty(
352322 }
353323 }
354324
325+ /// <summary>
326+ /// Removes any leftover leading end of lines on remaining members following any removed fields.
327+ /// </summary>
328+ /// <param name="fieldDeclarations">The collection of all fields that have been removed.</param>
329+ /// <param name="syntaxEditor">The <see cref="SyntaxEditor"/> instance to use.</param>
330+ private static void RemoveLeftoverLeadingEndOfLines ( IReadOnlyCollection < FieldDeclarationSyntax > fieldDeclarations , SyntaxEditor syntaxEditor )
331+ {
332+ foreach ( FieldDeclarationSyntax fieldDeclaration in fieldDeclarations )
333+ {
334+ // Special handling for the leading trivia of members following the field declaration we are about to remove.
335+ // There is an edge case that can happen when a type declaration is as follows:
336+ //
337+ // class ContainingType
338+ // {
339+ // public static readonly DependencyProperty NameProperty = ...;
340+ //
341+ // public void SomeOtherMember() { }
342+ //
343+ // public string? Name { ... }
344+ // }
345+ //
346+ // In this case, just removing the target field for the dependency property being rewritten (that is, 'NameProperty')
347+ // will cause an extra blank line to be left after the edits, right above the member immediately following the field.
348+ // To work around this, we look for such a member and check its trivia, and then manually remove a leading blank line.
349+ if ( fieldDeclaration . Parent is TypeDeclarationSyntax fieldParentTypeDeclaration )
350+ {
351+ int fieldDeclarationIndex = fieldParentTypeDeclaration . Members . IndexOf ( fieldDeclaration ) ;
352+
353+ // Check whether there is a member immediatley following the field
354+ if ( fieldDeclarationIndex >= 0 && fieldDeclarationIndex < fieldParentTypeDeclaration . Members . Count - 1 )
355+ {
356+ MemberDeclarationSyntax nextMember = fieldParentTypeDeclaration . Members [ fieldDeclarationIndex + 1 ] ;
357+
358+ // It's especially important to skip members that have been rmeoved. This would otherwise fail when computing
359+ // the final document. We only care about fixing trivia for members that will still be present after all edits.
360+ if ( fieldDeclarations . Contains ( nextMember ) )
361+ {
362+ continue ;
363+ }
364+
365+ SyntaxTriviaList leadingTrivia = nextMember . GetLeadingTrivia ( ) ;
366+
367+ // Check whether this member has a first leading trivia that's just a blank line: we want to remove this one
368+ if ( leadingTrivia . Count > 0 && leadingTrivia [ 0 ] . IsKind ( SyntaxKind . EndOfLineTrivia ) )
369+ {
370+ syntaxEditor . ReplaceNode ( nextMember , ( nextMember , _ ) => nextMember . WithLeadingTrivia ( leadingTrivia . RemoveAt ( 0 ) ) ) ;
371+ }
372+ }
373+ }
374+ }
375+ }
376+
355377 /// <summary>
356378 /// A custom <see cref="FixAllProvider"/> with the logic from <see cref="UsePartialPropertyForSemiAutoPropertyCodeFixer"/>.
357379 /// </summary>
@@ -381,6 +403,10 @@ private sealed class FixAllProvider : DocumentBasedFixAllProvider
381403 // Create an editor to perform all mutations (across all edits in the file)
382404 SyntaxEditor syntaxEditor = new ( root , fixAllContext . Solution . Services ) ;
383405
406+ // Create the set to track all fields being removed, to adjust whitespaces
407+ HashSet < FieldDeclarationSyntax > fieldDeclarations = [ ] ;
408+
409+ // Step 1: rewrite all properties and remove the fields
384410 foreach ( Diagnostic diagnostic in diagnostics )
385411 {
386412 // Get the current property declaration for the diagnostic
@@ -407,8 +433,13 @@ private sealed class FixAllProvider : DocumentBasedFixAllProvider
407433 generatedDependencyPropertyAttributeList ,
408434 syntaxEditor ,
409435 defaultValue ) ;
436+
437+ fieldDeclarations . Add ( fieldDeclaration ) ;
410438 }
411439
440+ // Step 2: remove any leftover leading end of lines on members following fields that have been removed
441+ RemoveLeftoverLeadingEndOfLines ( fieldDeclarations , syntaxEditor ) ;
442+
412443 return document . WithSyntaxRoot ( syntaxEditor . GetChangedRoot ( ) ) ;
413444 }
414445 }
0 commit comments