Skip to content

Commit 619f89b

Browse files
authored
Merge pull request swiftlang#107 from dylansturg/no_format_next
Support ignoring AST nodes using a comment, for specific node types.
2 parents d3bea2a + ac92a03 commit 619f89b

File tree

4 files changed

+418
-2
lines changed

4 files changed

+418
-2
lines changed

Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,6 +1015,11 @@ private final class TokenStreamCreator: SyntaxVisitor {
10151015
}
10161016

10171017
func visit(_ node: MemberDeclListItemSyntax) -> SyntaxVisitorContinueKind {
1018+
if shouldFormatterIgnore(node: node) {
1019+
appendFormatterIgnored(node: node)
1020+
return .skipChildren
1021+
}
1022+
10181023
before(node.firstToken, tokens: .open)
10191024
let resetSize = node.semicolon != nil ? 1 : 0
10201025
after(node.lastToken, tokens: .close, .break(.reset, size: resetSize))
@@ -1117,6 +1122,11 @@ private final class TokenStreamCreator: SyntaxVisitor {
11171122
}
11181123

11191124
func visit(_ node: CodeBlockItemSyntax) -> SyntaxVisitorContinueKind {
1125+
if shouldFormatterIgnore(node: node) {
1126+
appendFormatterIgnored(node: node)
1127+
return .skipChildren
1128+
}
1129+
11201130
before(node.firstToken, tokens: .open)
11211131
let resetSize = node.semicolon != nil ? 1 : 0
11221132
after(node.lastToken, tokens: .close, .break(.reset, size: resetSize))
@@ -2655,6 +2665,86 @@ private final class TokenStreamCreator: SyntaxVisitor {
26552665
// always parse the former as "4 +- 5".
26562666
return true
26572667
}
2668+
2669+
/// Appends the given node to the token stream without applying any formatting or printing tokens.
2670+
///
2671+
/// - Parameter node: A node that is ignored by the formatter.
2672+
private func appendFormatterIgnored(node: Syntax) {
2673+
// The first line of text in the `verbatim` token is printed with correct indentation, based on
2674+
// the previous tokens. The leading trivia of the first token needs to be excluded from the
2675+
// `verbatim` token in order for the first token to be printed with correct indentation. All
2676+
// following lines in the ignored node are printed as-is with no changes to indentation.
2677+
var nodeText = node.description
2678+
if let firstToken = node.firstToken {
2679+
extractLeadingTrivia(firstToken)
2680+
let leadingTriviaText = firstToken.leadingTrivia.reduce(into: "") { $1.write(to: &$0) }
2681+
nodeText = String(nodeText.dropFirst(leadingTriviaText.count))
2682+
}
2683+
2684+
// The leading trivia of the next token, after the ignored node, may contain content that
2685+
// belongs with the ignored node. The trivia extraction that is performed for `lastToken` later
2686+
// excludes that content so it needs to be extracted and added to the token stream here.
2687+
if let next = node.lastToken?.nextToken, let trivia = next.leadingTrivia.first {
2688+
switch trivia {
2689+
case .lineComment, .blockComment:
2690+
trivia.write(to: &nodeText)
2691+
break
2692+
default:
2693+
// All other kinds of trivia are inserted into the token stream by `extractLeadingTrivia`
2694+
// when the relevant token is visited.
2695+
break
2696+
}
2697+
}
2698+
2699+
appendToken(.verbatim(Verbatim(text: nodeText, indentingBehavior: .firstLine)))
2700+
2701+
// Add this break so that trivia parsing will allow discretionary newlines after the node.
2702+
appendToken(.break(.same, size: 0))
2703+
}
2704+
2705+
/// Returns whether the given trivia includes a directive to ignore formatting for the next node.
2706+
///
2707+
/// - Parameter trivia: Leading trivia for a node that the formatter supports ignoring.
2708+
private func isFormatterIgnorePresent(inTrivia trivia: Trivia) -> Bool {
2709+
func isFormatterIgnore(in commentText: String, prefix: String, suffix: String) -> Bool {
2710+
let trimmed =
2711+
commentText.dropFirst(prefix.count)
2712+
.dropLast(suffix.count)
2713+
.trimmingCharacters(in: .whitespaces)
2714+
return trimmed == "swift-format-ignore"
2715+
}
2716+
2717+
for piece in trivia {
2718+
switch piece {
2719+
case .lineComment(let text):
2720+
if isFormatterIgnore(in: text, prefix: "//", suffix: "") { return true }
2721+
break
2722+
case .blockComment(let text):
2723+
if isFormatterIgnore(in: text, prefix: "/*", suffix: "*/") { return true }
2724+
break
2725+
default:
2726+
break
2727+
}
2728+
}
2729+
return false
2730+
}
2731+
2732+
/// Returns whether the formatter should ignore the given node by printing it without changing the
2733+
/// node's internal text representation (i.e. print all text inside of the node as it was in the
2734+
/// original source).
2735+
///
2736+
/// - Note: The caller is responsible for ensuring that the given node is a type of node that can
2737+
/// be safely ignored.
2738+
///
2739+
/// - Parameter node: A node that can be safely ignored.
2740+
private func shouldFormatterIgnore(node: Syntax) -> Bool {
2741+
// Regardless of the level of nesting, if the ignore directive is present on the first token
2742+
// contained within the node then the entire node is eligible for ignoring.
2743+
if let firstTrivia = node.firstToken?.leadingTrivia {
2744+
return isFormatterIgnorePresent(inTrivia: firstTrivia)
2745+
}
2746+
return false
2747+
}
26582748
}
26592749

26602750
extension Syntax {

Sources/SwiftFormatPrettyPrint/Verbatim.swift

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,25 @@
1313
import Foundation
1414
import SwiftFormatConfiguration
1515

16+
/// Describes options for behavior when applying the indentation of the current context when
17+
/// printing a verbatim token.
18+
enum IndentingBehavior {
19+
/// The indentation of the current context is completely ignored.
20+
case none
21+
/// The indentation of the current context is applied to every line.
22+
case allLines
23+
/// The indentation of the current context is applied to the first line, and ignored on any
24+
/// additional lines.
25+
case firstLine
26+
}
27+
1628
struct Verbatim {
29+
let indentingBehavior: IndentingBehavior
1730
var lines: [String] = []
1831
var leadingWhitespaceCounts: [Int] = []
1932

20-
init(text: String) {
33+
init(text: String, indentingBehavior: IndentingBehavior = .allLines) {
34+
self.indentingBehavior = indentingBehavior
2135
tokenizeTextAndTrimWhitespace(text: text)
2236
}
2337

@@ -43,7 +57,13 @@ struct Verbatim {
4357
var output = ""
4458
for i in 0..<lines.count {
4559
if lines[i] != "" {
46-
output += indent.indentation()
60+
switch indentingBehavior {
61+
case .firstLine where i == 0, .allLines:
62+
output += indent.indentation()
63+
break
64+
case .none, .firstLine:
65+
break
66+
}
4767
output += String(repeating: " ", count: leadingWhitespaceCounts[i])
4868
output += lines[i]
4969
}

0 commit comments

Comments
 (0)