@@ -397,36 +397,97 @@ private class AttributeRemover: SyntaxRewriter {
397
397
var filteredAttributes : [ AttributeListSyntax . Element ] = [ ]
398
398
for case . attribute( let attribute) in node {
399
399
if attributesToRemove. contains ( attribute) {
400
- var leadingTrivia = node. leadingTrivia
401
- if let lastNewline = leadingTrivia. pieces. lastIndex ( where: { $0. isNewline } ) ,
400
+ var leadingTrivia = attribute. leadingTrivia
401
+
402
+ // Don't leave behind an empty line when the attribute being removed is on its own line,
403
+ // based on the following conditions:
404
+ // - Leading trivia ends with a newline followed by arbitrary number of spaces or tabs
405
+ // - All leading trivia pieces after the last newline are just whitespace, ensuring
406
+ // there are no comments or other non-whitespace characters on the same line
407
+ // preceding the attribute.
408
+ // - There is no trailing trivia and the next token has leading trivia.
409
+ if let lastNewline = leadingTrivia. pieces. lastIndex ( where: \. isNewline) ,
402
410
leadingTrivia. pieces [ lastNewline... ] . allSatisfy ( \. isWhitespace) ,
403
- node. trailingTrivia. isEmpty,
404
- node. nextToken ( viewMode: . sourceAccurate) ? . leadingTrivia. first? . isNewline ?? false
411
+ attribute. trailingTrivia. isEmpty,
412
+ let nextToken = attribute. nextToken ( viewMode: . sourceAccurate) ,
413
+ !nextToken. leadingTrivia. isEmpty
405
414
{
406
- // If the attribute is on its own line based on the following conditions,
407
- // remove the newline from it so we don’t end up with an empty line
408
- // - Trailing trivia ends with a newline followed by arbitrary number of spaces or tabs
409
- // - There is no trailing trivia and the next token starts on a new line
410
415
leadingTrivia = Trivia ( pieces: leadingTrivia. pieces [ ..< lastNewline] )
411
416
}
417
+
412
418
// Drop any spaces or tabs from the trailing trivia because there’s no
413
419
// more attribute they need to separate.
414
- let trailingTrivia = Trivia ( pieces : attribute. trailingTrivia. drop ( while: { $0 . isSpaceOrTab } ) )
420
+ let trailingTrivia = attribute. trailingTrivia. trimmingPrefix ( while: \ . isSpaceOrTab)
415
421
triviaToAttachToNextToken += leadingTrivia + trailingTrivia
422
+
423
+ // If the attribute is not separated from the previous attribute by trivia, as in
424
+ // `@First@Second var x: Int` (yes, that's valid Swift), removing the `@Second`
425
+ // attribute and dropping all its trivia would cause `@First` and `var` to join
426
+ // without any trivia in between, which is invalid. In such cases, the trailing trivia
427
+ // of the attribute is significant and must be retained.
428
+ if triviaToAttachToNextToken. isEmpty,
429
+ let previousToken = attribute. previousToken ( viewMode: . sourceAccurate) ,
430
+ previousToken. trailingTrivia. isEmpty
431
+ {
432
+ triviaToAttachToNextToken = attribute. trailingTrivia
433
+ }
416
434
} else {
417
- filteredAttributes. append ( . attribute( attribute) )
435
+ filteredAttributes. append ( . attribute( prependAndClearAccumulatedTrivia ( to: attribute) ) )
436
+ }
437
+ }
438
+
439
+ // Ensure that any horizontal whitespace trailing the attributes list is trimmed if the next
440
+ // token starts a new line.
441
+ if let nextToken = node. nextToken ( viewMode: . sourceAccurate) ,
442
+ nextToken. leadingTrivia. startsWithNewline
443
+ {
444
+ if !triviaToAttachToNextToken. isEmpty {
445
+ triviaToAttachToNextToken = triviaToAttachToNextToken. trimmingSuffix ( while: \. isSpaceOrTab)
446
+ } else if let lastAttribute = filteredAttributes. last {
447
+ filteredAttributes [ filteredAttributes. count - 1 ] . trailingTrivia = lastAttribute. trailingTrivia. trimmingSuffix ( while: \. isSpaceOrTab)
418
448
}
419
449
}
420
450
return AttributeListSyntax ( filteredAttributes)
421
451
}
422
452
423
453
override func visit( _ token: TokenSyntax ) -> TokenSyntax {
424
- if !triviaToAttachToNextToken. isEmpty {
425
- defer { triviaToAttachToNextToken = Trivia ( ) }
426
- return token. with ( \. leadingTrivia, triviaToAttachToNextToken + token. leadingTrivia)
427
- } else {
428
- return token
429
- }
454
+ return prependAndClearAccumulatedTrivia ( to: token)
455
+ }
456
+
457
+ /// Prepends the accumulated trivia to the given node's leading trivia.
458
+ ///
459
+ /// To preserve correct formatting after attribute removal, this function reassigns
460
+ /// significant trivia accumulated from removed attributes to the provided subsequent node.
461
+ /// Once attached, the accumulated trivia is cleared.
462
+ ///
463
+ /// - Parameter node: The syntax node receiving the accumulated trivia.
464
+ /// - Returns: The modified syntax node with the prepended trivia.
465
+ private func prependAndClearAccumulatedTrivia< T: SyntaxProtocol > ( to syntaxNode: T ) -> T {
466
+ defer { triviaToAttachToNextToken = Trivia ( ) }
467
+ return syntaxNode. with ( \. leadingTrivia, triviaToAttachToNextToken + syntaxNode. leadingTrivia)
468
+ }
469
+ }
470
+
471
+ private extension Trivia {
472
+ func trimmingPrefix(
473
+ while predicate: ( TriviaPiece ) -> Bool
474
+ ) -> Trivia {
475
+ Trivia ( pieces: self . drop ( while: predicate) )
476
+ }
477
+
478
+ func trimmingSuffix(
479
+ while predicate: ( TriviaPiece ) -> Bool
480
+ ) -> Trivia {
481
+ Trivia (
482
+ pieces: self [ ... ]
483
+ . reversed ( )
484
+ . drop ( while: predicate)
485
+ . reversed ( )
486
+ )
487
+ }
488
+
489
+ var startsWithNewline : Bool {
490
+ self . first? . isNewline ?? false
430
491
}
431
492
}
432
493
0 commit comments