@@ -447,10 +447,24 @@ private final class TokenStreamCreator: SyntaxVisitor {
447
447
448
448
arrangeBracesAndContents ( of: node. body, contentsKeyPath: \. statements)
449
449
450
- let elsePrecedingBreak = config. lineBreakBeforeControlFlowKeywords ? Token . newline : Token . space
451
- before ( node. elseKeyword, tokens: elsePrecedingBreak)
452
- if node. elseBody is IfStmtSyntax {
453
- after ( node. elseKeyword, tokens: . space)
450
+ if let elseKeyword = node. elseKeyword {
451
+ // Add a token before the else keyword. Breaking before `else` is explicitly allowed when
452
+ // there's a comment.
453
+ if config. lineBreakBeforeControlFlowKeywords {
454
+ before ( elseKeyword, tokens: . newline)
455
+ } else if elseKeyword. leadingTrivia. hasLineComment {
456
+ before ( elseKeyword, tokens: . break( . same, size: 1 ) )
457
+ } else {
458
+ before ( elseKeyword, tokens: . space)
459
+ }
460
+
461
+ // Breaks are only allowed after `else` when there's a comment; otherwise there shouldn't be
462
+ // any newlines between `else` and the open brace or a following `if`.
463
+ if let tokenAfterElse = elseKeyword. nextToken, tokenAfterElse. leadingTrivia. hasLineComment {
464
+ after ( node. elseKeyword, tokens: . break( . same, size: 1 ) )
465
+ } else if node. elseBody is IfStmtSyntax {
466
+ after ( node. elseKeyword, tokens: . space)
467
+ }
454
468
}
455
469
456
470
arrangeBracesAndContents ( of: node. elseBody as? CodeBlockSyntax , contentsKeyPath: \. statements)
@@ -631,6 +645,14 @@ private final class TokenStreamCreator: SyntaxVisitor {
631
645
// `stackedIndentationBehavior`).
632
646
after ( node. leftParen, tokens: . open)
633
647
before ( node. rightParen, tokens: . close)
648
+
649
+ // When there's a comment inside of a parenthesized expression, we want to allow the comment
650
+ // to exist at the EOL with the left paren or on its own line. The contents are always
651
+ // indented on the following lines, since parens always create a scope. An open/close break
652
+ // pair isn't used here to avoid forcing the closing paren down onto a new line.
653
+ if node. leftParen. nextToken? . leadingTrivia. hasLineComment ?? false {
654
+ after ( node. leftParen, tokens: . break( . continue, size: 0 ) )
655
+ }
634
656
} else if elementCount > 1 {
635
657
// Tuples with more than one element are "true" tuples, and should indent as block structures.
636
658
after ( node. leftParen, tokens: . break( . open, size: 0 ) , . open)
@@ -2717,50 +2739,6 @@ private final class TokenStreamCreator: SyntaxVisitor {
2717
2739
// Add this break so that trivia parsing will allow discretionary newlines after the node.
2718
2740
appendToken ( . break( . same, size: 0 ) )
2719
2741
}
2720
-
2721
- /// Returns whether the given trivia includes a directive to ignore formatting for the next node.
2722
- ///
2723
- /// - Parameter trivia: Leading trivia for a node that the formatter supports ignoring.
2724
- private func isFormatterIgnorePresent( inTrivia trivia: Trivia ) -> Bool {
2725
- func isFormatterIgnore( in commentText: String , prefix: String , suffix: String ) -> Bool {
2726
- let trimmed =
2727
- commentText. dropFirst ( prefix. count)
2728
- . dropLast ( suffix. count)
2729
- . trimmingCharacters ( in: . whitespaces)
2730
- return trimmed == " swift-format-ignore "
2731
- }
2732
-
2733
- for piece in trivia {
2734
- switch piece {
2735
- case . lineComment( let text) :
2736
- if isFormatterIgnore ( in: text, prefix: " // " , suffix: " " ) { return true }
2737
- break
2738
- case . blockComment( let text) :
2739
- if isFormatterIgnore ( in: text, prefix: " /*", suffix: "*/" ) { return true }
2740
- break
2741
- default :
2742
- break
2743
- }
2744
- }
2745
- return false
2746
- }
2747
-
2748
- /// Returns whether the formatter should ignore the given node by printing it without changing the
2749
- /// node's internal text representation (i.e. print all text inside of the node as it was in the
2750
- /// original source).
2751
- ///
2752
- /// - Note: The caller is responsible for ensuring that the given node is a type of node that can
2753
- /// be safely ignored.
2754
- ///
2755
- /// - Parameter node: A node that can be safely ignored.
2756
- private func shouldFormatterIgnore( node: Syntax ) -> Bool {
2757
- // Regardless of the level of nesting, if the ignore directive is present on the first token
2758
- // contained within the node then the entire node is eligible for ignoring.
2759
- if let firstTrivia = node. firstToken? . leadingTrivia {
2760
- return isFormatterIgnorePresent ( inTrivia: firstTrivia)
2761
- }
2762
- return false
2763
- }
2764
2742
}
2765
2743
2766
2744
extension Syntax {
@@ -2769,8 +2747,10 @@ extension Syntax {
2769
2747
// First, fold the sequence expressions in the tree before passing it along to the token stream,
2770
2748
// because we don't want to modify the tree during token stream creation.
2771
2749
let folded = SequenceExprFoldingRewriter ( operatorContext: operatorContext) . visit ( self )
2750
+ let commentsMoved = CommentMovingRewriter ( ) . visit ( folded)
2772
2751
return TokenStreamCreator (
2773
- configuration: configuration, operatorContext: operatorContext) . makeStream ( from: folded)
2752
+ configuration: configuration, operatorContext: operatorContext)
2753
+ . makeStream ( from: commentsMoved)
2774
2754
}
2775
2755
}
2776
2756
@@ -2792,8 +2772,138 @@ class SequenceExprFoldingRewriter: SyntaxRewriter {
2792
2772
}
2793
2773
}
2794
2774
2775
+ /// Rewriter that relocates comment trivia around nodes where comments are known to be better
2776
+ /// formatted when placed before or after the node.
2777
+ ///
2778
+ /// For example, comments after binary operators are relocated to be before the operator, which
2779
+ /// results in fewer line breaks with the comment closer to the relevant tokens.
2780
+ class CommentMovingRewriter : SyntaxRewriter {
2781
+ /// Map of tokens to alternate trivia to use as the token's leading trivia.
2782
+ var rewriteTokenTriviaMap : [ TokenSyntax : Trivia ] = [ : ]
2783
+
2784
+ override func visit( _ node: CodeBlockItemSyntax ) -> Syntax {
2785
+ if shouldFormatterIgnore ( node: node) {
2786
+ return node
2787
+ }
2788
+ return super. visit ( node)
2789
+ }
2790
+
2791
+ override func visit( _ node: MemberDeclListItemSyntax ) -> Syntax {
2792
+ if shouldFormatterIgnore ( node: node) {
2793
+ return node
2794
+ }
2795
+ return super. visit ( node)
2796
+ }
2797
+
2798
+ override func visit( _ token: TokenSyntax ) -> Syntax {
2799
+ if let rewrittenTrivia = rewriteTokenTriviaMap [ token] {
2800
+ return token. withLeadingTrivia ( rewrittenTrivia)
2801
+ }
2802
+ return token
2803
+ }
2804
+
2805
+ override func visit( _ node: SequenceExprSyntax ) -> ExprSyntax {
2806
+ for element in node. elements {
2807
+ if let binaryOperatorExpr = element as? BinaryOperatorExprSyntax ,
2808
+ let followingToken = binaryOperatorExpr. operatorToken. nextToken,
2809
+ followingToken. leadingTrivia. hasLineComment
2810
+ {
2811
+ // Rewrite the trivia so that the comment is in the operator token's leading trivia.
2812
+ let ( remainingTrivia, extractedTrivia) = extractLineCommentTrivia ( from: followingToken)
2813
+ let combinedTrivia = binaryOperatorExpr. operatorToken. leadingTrivia + extractedTrivia
2814
+ rewriteTokenTriviaMap [ binaryOperatorExpr. operatorToken] = combinedTrivia
2815
+ rewriteTokenTriviaMap [ followingToken] = remainingTrivia
2816
+ }
2817
+ }
2818
+ return super. visit ( node)
2819
+ }
2820
+
2821
+ /// Extracts trivia containing and related to line comments from `token`'s leading trivia. Returns
2822
+ /// 2 trivia collections: the trivia that wasn't extracted and should remain in `token`'s leading
2823
+ /// trivia and the trivia that meets the criteria for extraction.
2824
+ /// - Parameter token: A token whose leading trivia should be split to extract line comments.
2825
+ private func extractLineCommentTrivia( from token: TokenSyntax ) -> (
2826
+ remainingTrivia: Trivia , extractedTrivia: Trivia
2827
+ ) {
2828
+ var pendingPieces = [ TriviaPiece] ( )
2829
+ var keepWithTokenPieces = [ TriviaPiece] ( )
2830
+ var extractingPieces = [ TriviaPiece] ( )
2831
+
2832
+ // Line comments and adjacent newlines are extracted so they can be moved to a different token's
2833
+ // leading trivia, while all other kinds of tokens are left as-is.
2834
+ var lastPiece : TriviaPiece ?
2835
+ for piece in token. leadingTrivia {
2836
+ defer { lastPiece = piece }
2837
+ switch piece {
2838
+ case . lineComment:
2839
+ extractingPieces. append ( contentsOf: pendingPieces)
2840
+ pendingPieces. removeAll ( )
2841
+ extractingPieces. append ( piece)
2842
+ case . blockComment, . docLineComment, . docBlockComment:
2843
+ keepWithTokenPieces. append ( contentsOf: pendingPieces)
2844
+ pendingPieces. removeAll ( )
2845
+ keepWithTokenPieces. append ( piece)
2846
+ case . newlines, . carriageReturns, . carriageReturnLineFeeds:
2847
+ if case . lineComment = lastPiece {
2848
+ extractingPieces. append ( piece)
2849
+ } else {
2850
+ pendingPieces. append ( piece)
2851
+ }
2852
+ default :
2853
+ pendingPieces. append ( piece)
2854
+ }
2855
+ }
2856
+ keepWithTokenPieces. append ( contentsOf: pendingPieces)
2857
+ return ( Trivia ( pieces: keepWithTokenPieces) , Trivia ( pieces: extractingPieces) )
2858
+ }
2859
+ }
2860
+
2795
2861
extension Collection {
2796
2862
subscript( safe index: Index ) -> Element ? {
2797
2863
return index < endIndex ? self [ index] : nil
2798
2864
}
2799
2865
}
2866
+
2867
+ /// Returns whether the given trivia includes a directive to ignore formatting for the next node.
2868
+ ///
2869
+ /// - Parameter trivia: Leading trivia for a node that the formatter supports ignoring.
2870
+ private func isFormatterIgnorePresent( inTrivia trivia: Trivia ) -> Bool {
2871
+ func isFormatterIgnore( in commentText: String , prefix: String , suffix: String ) -> Bool {
2872
+ let trimmed =
2873
+ commentText. dropFirst ( prefix. count)
2874
+ . dropLast ( suffix. count)
2875
+ . trimmingCharacters ( in: . whitespaces)
2876
+ return trimmed == " swift-format-ignore "
2877
+ }
2878
+
2879
+ for piece in trivia {
2880
+ switch piece {
2881
+ case . lineComment( let text) :
2882
+ if isFormatterIgnore ( in: text, prefix: " // " , suffix: " " ) { return true }
2883
+ break
2884
+ case . blockComment( let text) :
2885
+ if isFormatterIgnore ( in: text, prefix: " /*", suffix: "*/" ) { return true }
2886
+ break
2887
+ default :
2888
+ break
2889
+ }
2890
+ }
2891
+ return false
2892
+ }
2893
+
2894
+ /// Returns whether the formatter should ignore the given node by printing it without changing the
2895
+ /// node's internal text representation (i.e. print all text inside of the node as it was in the
2896
+ /// original source).
2897
+ ///
2898
+ /// - Note: The caller is responsible for ensuring that the given node is a type of node that can
2899
+ /// be safely ignored.
2900
+ ///
2901
+ /// - Parameter node: A node that can be safely ignored.
2902
+ private func shouldFormatterIgnore( node: Syntax ) -> Bool {
2903
+ // Regardless of the level of nesting, if the ignore directive is present on the first token
2904
+ // contained within the node then the entire node is eligible for ignoring.
2905
+ if let firstTrivia = node. firstToken? . leadingTrivia {
2906
+ return isFormatterIgnorePresent ( inTrivia: firstTrivia)
2907
+ }
2908
+ return false
2909
+ }
0 commit comments