Skip to content

Commit 17bc997

Browse files
committed
Fix handling of EOLs in removed members
1 parent 7d5c559 commit 17bc997

File tree

1 file changed

+64
-33
lines changed

1 file changed

+64
-33
lines changed

components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs

Lines changed: 64 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
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;
56
using System.Collections.Immutable;
67
using System.Composition;
78
using 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

Comments
 (0)