Skip to content

Commit ac92a03

Browse files
committed
Support ignoring AST nodes using a comment, for specific node types.
Originally, I had hoped to support ignoring any type of node by simply including a comment before the node. That turned out to be infeasible due to an existing reliance on eventually visiting every TokenSyntax in the AST. For example, some nodes attach printing tokens before or after TokenSyntax instances that will be visited later. In the event that a TokenSyntax is ignored, those attached before/after tokens are also ignored leading to mismatches in the token stream. Rather than to fix all such instances of that dependency, which may not itself be feasible, I've instead limited the ignore directive to 2 "top level" types of nodes: code block list items and member decl list items. This allows ignoring on an entire statement or member. Verbatim token changes: I had to add a new option on `Verbatim` to support only indenting the first line of the text contained in the token. Originally, verbatim always printed the indentation of the current context on every line of text. That breaks when verbatim is used to print text that's nested in other decls. Each time the formatter runs, the verbatim text is indented by its original indentation + the indentation of the current context.
1 parent 4837fec commit ac92a03

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
@@ -981,6 +981,11 @@ private final class TokenStreamCreator: SyntaxVisitor {
981981
}
982982

983983
func visit(_ node: MemberDeclListItemSyntax) -> SyntaxVisitorContinueKind {
984+
if shouldFormatterIgnore(node: node) {
985+
appendFormatterIgnored(node: node)
986+
return .skipChildren
987+
}
988+
984989
before(node.firstToken, tokens: .open)
985990
let resetSize = node.semicolon != nil ? 1 : 0
986991
after(node.lastToken, tokens: .close, .break(.reset, size: resetSize))
@@ -1083,6 +1088,11 @@ private final class TokenStreamCreator: SyntaxVisitor {
10831088
}
10841089

10851090
func visit(_ node: CodeBlockItemSyntax) -> SyntaxVisitorContinueKind {
1091+
if shouldFormatterIgnore(node: node) {
1092+
appendFormatterIgnored(node: node)
1093+
return .skipChildren
1094+
}
1095+
10861096
before(node.firstToken, tokens: .open)
10871097
let resetSize = node.semicolon != nil ? 1 : 0
10881098
after(node.lastToken, tokens: .close, .break(.reset, size: resetSize))
@@ -2558,6 +2568,86 @@ private final class TokenStreamCreator: SyntaxVisitor {
25582568
// always parse the former as "4 +- 5".
25592569
return true
25602570
}
2571+
2572+
/// Appends the given node to the token stream without applying any formatting or printing tokens.
2573+
///
2574+
/// - Parameter node: A node that is ignored by the formatter.
2575+
private func appendFormatterIgnored(node: Syntax) {
2576+
// The first line of text in the `verbatim` token is printed with correct indentation, based on
2577+
// the previous tokens. The leading trivia of the first token needs to be excluded from the
2578+
// `verbatim` token in order for the first token to be printed with correct indentation. All
2579+
// following lines in the ignored node are printed as-is with no changes to indentation.
2580+
var nodeText = node.description
2581+
if let firstToken = node.firstToken {
2582+
extractLeadingTrivia(firstToken)
2583+
let leadingTriviaText = firstToken.leadingTrivia.reduce(into: "") { $1.write(to: &$0) }
2584+
nodeText = String(nodeText.dropFirst(leadingTriviaText.count))
2585+
}
2586+
2587+
// The leading trivia of the next token, after the ignored node, may contain content that
2588+
// belongs with the ignored node. The trivia extraction that is performed for `lastToken` later
2589+
// excludes that content so it needs to be extracted and added to the token stream here.
2590+
if let next = node.lastToken?.nextToken, let trivia = next.leadingTrivia.first {
2591+
switch trivia {
2592+
case .lineComment, .blockComment:
2593+
trivia.write(to: &nodeText)
2594+
break
2595+
default:
2596+
// All other kinds of trivia are inserted into the token stream by `extractLeadingTrivia`
2597+
// when the relevant token is visited.
2598+
break
2599+
}
2600+
}
2601+
2602+
appendToken(.verbatim(Verbatim(text: nodeText, indentingBehavior: .firstLine)))
2603+
2604+
// Add this break so that trivia parsing will allow discretionary newlines after the node.
2605+
appendToken(.break(.same, size: 0))
2606+
}
2607+
2608+
/// Returns whether the given trivia includes a directive to ignore formatting for the next node.
2609+
///
2610+
/// - Parameter trivia: Leading trivia for a node that the formatter supports ignoring.
2611+
private func isFormatterIgnorePresent(inTrivia trivia: Trivia) -> Bool {
2612+
func isFormatterIgnore(in commentText: String, prefix: String, suffix: String) -> Bool {
2613+
let trimmed =
2614+
commentText.dropFirst(prefix.count)
2615+
.dropLast(suffix.count)
2616+
.trimmingCharacters(in: .whitespaces)
2617+
return trimmed == "swift-format-ignore"
2618+
}
2619+
2620+
for piece in trivia {
2621+
switch piece {
2622+
case .lineComment(let text):
2623+
if isFormatterIgnore(in: text, prefix: "//", suffix: "") { return true }
2624+
break
2625+
case .blockComment(let text):
2626+
if isFormatterIgnore(in: text, prefix: "/*", suffix: "*/") { return true }
2627+
break
2628+
default:
2629+
break
2630+
}
2631+
}
2632+
return false
2633+
}
2634+
2635+
/// Returns whether the formatter should ignore the given node by printing it without changing the
2636+
/// node's internal text representation (i.e. print all text inside of the node as it was in the
2637+
/// original source).
2638+
///
2639+
/// - Note: The caller is responsible for ensuring that the given node is a type of node that can
2640+
/// be safely ignored.
2641+
///
2642+
/// - Parameter node: A node that can be safely ignored.
2643+
private func shouldFormatterIgnore(node: Syntax) -> Bool {
2644+
// Regardless of the level of nesting, if the ignore directive is present on the first token
2645+
// contained within the node then the entire node is eligible for ignoring.
2646+
if let firstTrivia = node.firstToken?.leadingTrivia {
2647+
return isFormatterIgnorePresent(inTrivia: firstTrivia)
2648+
}
2649+
return false
2650+
}
25612651
}
25622652

25632653
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)