Skip to content

Commit 15959b2

Browse files
committed
Use the SwiftOperators module to fold sequence expressions.
This change deletes the custom folding implementation that was used previously in swift-format. Folding was previously only performed before the pretty-printing stage (after the "rules" stage), but now folding occurs before the folding stage, allowing us to write rules that are aware of operator precedence. The exception to the above is that if you use the `format` or `lint` APIs that take a `SourceFileSyntax`, you must manually fold the syntax tree first (any `SequenceExpr`s left in the tree will trap).
1 parent 475313f commit 15959b2

File tree

14 files changed

+273
-1403
lines changed

14 files changed

+273
-1403
lines changed

Package.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ let package = Package(
4646
"SwiftFormatRules",
4747
"SwiftFormatWhitespaceLinter",
4848
.product(name: "SwiftSyntax", package: "swift-syntax"),
49+
.product(name: "SwiftOperators", package: "swift-syntax"),
4950
.product(name: "SwiftParser", package: "swift-syntax"),
5051
]
5152
),
@@ -56,6 +57,7 @@ let package = Package(
5657
name: "SwiftFormatCore",
5758
dependencies: [
5859
"SwiftFormatConfiguration",
60+
.product(name: "SwiftOperators", package: "swift-syntax"),
5961
.product(name: "SwiftSyntax", package: "swift-syntax"),
6062
]
6163
),
@@ -65,14 +67,19 @@ let package = Package(
6567
),
6668
.target(
6769
name: "SwiftFormatPrettyPrint",
68-
dependencies: ["SwiftFormatCore", "SwiftFormatConfiguration"]
70+
dependencies: [
71+
"SwiftFormatCore",
72+
"SwiftFormatConfiguration",
73+
.product(name: "SwiftOperators", package: "swift-syntax"),
74+
]
6975
),
7076
.target(
7177
name: "SwiftFormatTestSupport",
7278
dependencies: [
7379
"SwiftFormatCore",
7480
"SwiftFormatRules",
7581
"SwiftFormatConfiguration",
82+
.product(name: "SwiftOperators", package: "swift-syntax"),
7683
]
7784
),
7885
.target(
@@ -144,6 +151,7 @@ let package = Package(
144151
"SwiftFormatRules",
145152
"SwiftFormatTestSupport",
146153
.product(name: "SwiftSyntax", package: "swift-syntax"),
154+
.product(name: "SwiftOperators", package: "swift-syntax"),
147155
.product(name: "SwiftParser", package: "swift-syntax"),
148156
]
149157
),

Sources/SwiftFormat/Parsing.swift

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
import SwiftDiagnostics
15+
import SwiftOperators
16+
import SwiftParser
17+
import SwiftSyntax
18+
19+
/// Parses the given source code and returns a valid `SourceFileSyntax` node.
20+
///
21+
/// This helper function automatically folds sequence expressions using the given operator table,
22+
/// ignoring errors so that formatting can do something reasonable in the presence of unrecognized
23+
/// operators.
24+
///
25+
/// - Parameters:
26+
/// - source: The Swift source code to be formatted.
27+
/// - url: A file URL denoting the filename/path that should be assumed for this syntax tree,
28+
/// which is associated with any diagnostics emitted during formatting. If this is nil, a
29+
/// dummy value will be used.
30+
/// - operatorTable: The operator table to use for sequence folding.
31+
/// - parsingDiagnosticHandler: An optional callback that will be notified if there are any
32+
/// errors when parsing the source code.
33+
/// - Throws: If an unrecoverable error occurs when formatting the code.
34+
func parseAndEmitDiagnostics(
35+
source: String,
36+
operatorTable: OperatorTable,
37+
assumingFileURL url: URL?,
38+
parsingDiagnosticHandler: ((Diagnostic, SourceLocation) -> Void)? = nil
39+
) throws -> SourceFileSyntax {
40+
let sourceFile =
41+
try operatorTable.foldAll(Parser.parse(source: source)) { _ in }.as(SourceFileSyntax.self)!
42+
43+
if let parsingDiagnosticHandler = parsingDiagnosticHandler {
44+
let expectedConverter =
45+
SourceLocationConverter(file: url?.path ?? "<unknown>", tree: sourceFile)
46+
let diagnostics = ParseDiagnosticsGenerator.diagnostics(for: sourceFile)
47+
for diagnostic in diagnostics {
48+
let location = diagnostic.location(converter: expectedConverter)
49+
parsingDiagnosticHandler(diagnostic, location)
50+
}
51+
}
52+
53+
return sourceFile
54+
}

Sources/SwiftFormat/SwiftFormatter.swift

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import Foundation
14+
import SwiftDiagnostics
1415
import SwiftFormatConfiguration
1516
import SwiftFormatCore
1617
import SwiftFormatPrettyPrint
1718
import SwiftFormatRules
19+
import SwiftOperators
1820
import SwiftSyntax
19-
import SwiftParser
20-
import SwiftDiagnostics
2121

2222
/// Formats Swift source code or syntax trees according to the Swift style guidelines.
2323
public final class SwiftFormatter {
@@ -46,6 +46,11 @@ public final class SwiftFormatter {
4646

4747
/// Formats the Swift code at the given file URL and writes the result to an output stream.
4848
///
49+
/// This form of the `format` function automatically folds expressions using the default operator
50+
/// set defined in Swift. If you need more control over this—for example, to provide the correct
51+
/// precedence relationships for custom operators—you must parse and fold the syntax tree
52+
/// manually and then call ``format(syntax:assumingFileURL:to:)``.
53+
///
4954
/// - Parameters:
5055
/// - url: The URL of the file containing the code to format.
5156
/// - outputStream: A value conforming to `TextOutputStream` to which the formatted output will
@@ -66,20 +71,23 @@ public final class SwiftFormatter {
6671
throw SwiftFormatError.isDirectory
6772
}
6873
let source = try String(contentsOf: url, encoding: .utf8)
69-
let sourceFile = try Parser.parse(source: source)
70-
if let parsingDiagnosticHandler = parsingDiagnosticHandler {
71-
let expectedConverter = SourceLocationConverter(file: url.path, tree: sourceFile)
72-
let diagnostics = ParseDiagnosticsGenerator.diagnostics(for: sourceFile)
73-
for diagnostic in diagnostics {
74-
let location = diagnostic.location(converter: expectedConverter)
75-
parsingDiagnosticHandler(diagnostic, location)
76-
}
77-
}
78-
try format(syntax: sourceFile, assumingFileURL: url, source: source, to: &outputStream)
74+
let sourceFile = try parseAndEmitDiagnostics(
75+
source: source,
76+
operatorTable: .standardOperators,
77+
assumingFileURL: url,
78+
parsingDiagnosticHandler: parsingDiagnosticHandler)
79+
try format(
80+
syntax: sourceFile, operatorTable: .standardOperators, assumingFileURL: url, source: source,
81+
to: &outputStream)
7982
}
8083

8184
/// Formats the given Swift source code and writes the result to an output stream.
8285
///
86+
/// This form of the `format` function automatically folds expressions using the default operator
87+
/// set defined in Swift. If you need more control over this—for example, to provide the correct
88+
/// precedence relationships for custom operators—you must parse and fold the syntax tree
89+
/// manually and then call ``format(syntax:assumingFileURL:to:)``.
90+
///
8391
/// - Parameters:
8492
/// - source: The Swift source code to be formatted.
8593
/// - url: A file URL denoting the filename/path that should be assumed for this syntax tree,
@@ -96,48 +104,56 @@ public final class SwiftFormatter {
96104
to outputStream: inout Output,
97105
parsingDiagnosticHandler: ((Diagnostic, SourceLocation) -> Void)? = nil
98106
) throws {
99-
let sourceFile = try Parser.parse(source: source)
100-
if let parsingDiagnosticHandler = parsingDiagnosticHandler {
101-
let expectedConverter = SourceLocationConverter(file: url?.path ?? "<unknown>", tree: sourceFile)
102-
let diagnostics = ParseDiagnosticsGenerator.diagnostics(for: sourceFile)
103-
for diagnostic in diagnostics {
104-
let location = diagnostic.location(converter: expectedConverter)
105-
parsingDiagnosticHandler(diagnostic, location)
106-
}
107-
}
108-
try format(syntax: sourceFile, assumingFileURL: url, source: source, to: &outputStream)
107+
let sourceFile = try parseAndEmitDiagnostics(
108+
source: source,
109+
operatorTable: .standardOperators,
110+
assumingFileURL: url,
111+
parsingDiagnosticHandler: parsingDiagnosticHandler)
112+
try format(
113+
syntax: sourceFile, operatorTable: .standardOperators, assumingFileURL: url, source: source,
114+
to: &outputStream)
109115
}
110116

111117
/// Formats the given Swift syntax tree and writes the result to an output stream.
112118
///
119+
/// This form of the `format` function does not perform any additional processing on the given
120+
/// syntax tree. The tree **must** have all expressions folded using an `OperatorTable`, and no
121+
/// detection of warnings/errors is performed.
122+
///
113123
/// - Note: The formatter may be faster using the source text, if it's available.
114124
///
115125
/// - Parameters:
116126
/// - syntax: The Swift syntax tree to be converted to source code and formatted.
127+
/// - operatorTable: The table that defines the operators and their precedence relationships.
128+
/// This must be the same operator table that was used to fold the expressions in the `syntax`
129+
/// argument.
117130
/// - url: A file URL denoting the filename/path that should be assumed for this syntax tree,
118131
/// which is associated with any diagnostics emitted during formatting. If this is nil, a
119132
/// dummy value will be used.
120133
/// - outputStream: A value conforming to `TextOutputStream` to which the formatted output will
121134
/// be written.
122135
/// - Throws: If an unrecoverable error occurs when formatting the code.
123136
public func format<Output: TextOutputStream>(
124-
syntax: SourceFileSyntax, assumingFileURL url: URL?, to outputStream: inout Output
137+
syntax: SourceFileSyntax, operatorTable: OperatorTable, assumingFileURL url: URL?,
138+
to outputStream: inout Output
125139
) throws {
126-
try format(syntax: syntax, assumingFileURL: url, source: nil, to: &outputStream)
140+
try format(
141+
syntax: syntax, operatorTable: operatorTable, assumingFileURL: url, source: nil,
142+
to: &outputStream)
127143
}
128144

129145
private func format<Output: TextOutputStream>(
130-
syntax: SourceFileSyntax, assumingFileURL url: URL?, source: String?,
131-
to outputStream: inout Output
146+
syntax: SourceFileSyntax, operatorTable: OperatorTable,
147+
assumingFileURL url: URL?, source: String?, to outputStream: inout Output
132148
) throws {
133149
if let position = _firstInvalidSyntaxPosition(in: Syntax(syntax)) {
134150
throw SwiftFormatError.fileContainsInvalidSyntax(position: position)
135151
}
136152

137153
let assumedURL = url ?? URL(fileURLWithPath: "source")
138154
let context = Context(
139-
configuration: configuration, findingConsumer: findingConsumer, fileURL: assumedURL,
140-
sourceFileSyntax: syntax, source: source, ruleNameCache: ruleNameCache)
155+
configuration: configuration, operatorTable: operatorTable, findingConsumer: findingConsumer,
156+
fileURL: assumedURL, sourceFileSyntax: syntax, source: source, ruleNameCache: ruleNameCache)
141157
let pipeline = FormatPipeline(context: context)
142158
let transformedSyntax = pipeline.visit(Syntax(syntax))
143159

@@ -146,10 +162,8 @@ public final class SwiftFormatter {
146162
return
147163
}
148164

149-
let operatorContext = OperatorContext.makeBuiltinOperatorContext()
150165
let printer = PrettyPrinter(
151166
context: context,
152-
operatorContext: operatorContext,
153167
node: transformedSyntax,
154168
printTokenStream: debugOptions.contains(.dumpTokenStream),
155169
whitespaceOnly: false)

Sources/SwiftFormat/SwiftLinter.swift

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import Foundation
14+
import SwiftDiagnostics
1415
import SwiftFormatConfiguration
1516
import SwiftFormatCore
1617
import SwiftFormatPrettyPrint
1718
import SwiftFormatRules
1819
import SwiftFormatWhitespaceLinter
20+
import SwiftOperators
1921
import SwiftSyntax
20-
import SwiftParser
21-
import SwiftDiagnostics
2222

2323
/// Diagnoses and reports problems in Swift source code or syntax trees according to the Swift style
2424
/// guidelines.
@@ -47,6 +47,11 @@ public final class SwiftLinter {
4747

4848
/// Lints the Swift code at the given file URL.
4949
///
50+
/// This form of the `lint` function automatically folds expressions using the default operator
51+
/// set defined in Swift. If you need more control over this—for example, to provide the correct
52+
/// precedence relationships for custom operators—you must parse and fold the syntax tree
53+
/// manually and then call ``lint(syntax:assumingFileURL:)``.
54+
///
5055
/// - Parameters:
5156
/// - url: The URL of the file containing the code to format.
5257
/// - parsingDiagnosticHandler: An optional callback that will be notified if there are any
@@ -64,13 +69,22 @@ public final class SwiftLinter {
6469
throw SwiftFormatError.isDirectory
6570
}
6671
let source = try String(contentsOf: url, encoding: .utf8)
67-
let sourceFile = try Parser.parse(source: source)
68-
try lint(syntax: sourceFile, assumingFileURL: url,
69-
source: source, parsingDiagnosticHandler: parsingDiagnosticHandler)
72+
let sourceFile = try parseAndEmitDiagnostics(
73+
source: source,
74+
operatorTable: .standardOperators,
75+
assumingFileURL: url,
76+
parsingDiagnosticHandler: parsingDiagnosticHandler)
77+
try lint(
78+
syntax: sourceFile, operatorTable: .standardOperators, assumingFileURL: url, source: source)
7079
}
7180

7281
/// Lints the given Swift source code.
7382
///
83+
/// This form of the `lint` function automatically folds expressions using the default operator
84+
/// set defined in Swift. If you need more control over this—for example, to provide the correct
85+
/// precedence relationships for custom operators—you must parse and fold the syntax tree
86+
/// manually and then call ``lint(syntax:assumingFileURL:)``.
87+
///
7488
/// - Parameters:
7589
/// - source: The Swift source code to be linted.
7690
/// - url: A file URL denoting the filename/path that should be assumed for this source code.
@@ -82,46 +96,51 @@ public final class SwiftLinter {
8296
assumingFileURL url: URL,
8397
parsingDiagnosticHandler: ((Diagnostic, SourceLocation) -> Void)? = nil
8498
) throws {
85-
let sourceFile = try Parser.parse(source: source)
86-
try lint(syntax: sourceFile, assumingFileURL: url,
87-
source: source, parsingDiagnosticHandler: parsingDiagnosticHandler)
99+
let sourceFile = try parseAndEmitDiagnostics(
100+
source: source,
101+
operatorTable: .standardOperators,
102+
assumingFileURL: url,
103+
parsingDiagnosticHandler: parsingDiagnosticHandler)
104+
try lint(
105+
syntax: sourceFile, operatorTable: .standardOperators, assumingFileURL: url, source: source)
88106
}
89107

90108
/// Lints the given Swift syntax tree.
91109
///
110+
/// This form of the `lint` function does not perform any additional processing on the given
111+
/// syntax tree. The tree **must** have all expressions folded using an `OperatorTable`, and no
112+
/// detection of warnings/errors is performed.
113+
///
92114
/// - Note: The linter may be faster using the source text, if it's available.
93115
///
94116
/// - Parameters:
95117
/// - syntax: The Swift syntax tree to be converted to be linted.
118+
/// - operatorTable: The table that defines the operators and their precedence relationships.
119+
/// This must be the same operator table that was used to fold the expressions in the `syntax`
120+
/// argument.
96121
/// - url: A file URL denoting the filename/path that should be assumed for this syntax tree.
97122
/// - Throws: If an unrecoverable error occurs when formatting the code.
98-
public func lint(syntax: SourceFileSyntax, assumingFileURL url: URL) throws {
99-
try lint(syntax: syntax, assumingFileURL: url,
100-
source: nil, parsingDiagnosticHandler: nil)
123+
public func lint(
124+
syntax: SourceFileSyntax,
125+
operatorTable: OperatorTable,
126+
assumingFileURL url: URL
127+
) throws {
128+
try lint(syntax: syntax, operatorTable: operatorTable, assumingFileURL: url, source: nil)
101129
}
102130

103131
private func lint(
104132
syntax: SourceFileSyntax,
133+
operatorTable: OperatorTable,
105134
assumingFileURL url: URL,
106-
source: String?,
107-
parsingDiagnosticHandler: ((Diagnostic, SourceLocation) -> Void)?
135+
source: String?
108136
) throws {
109137
if let position = _firstInvalidSyntaxPosition(in: Syntax(syntax)) {
110138
throw SwiftFormatError.fileContainsInvalidSyntax(position: position)
111139
}
112140

113-
if let parsingDiagnosticHandler = parsingDiagnosticHandler {
114-
let expectedConverter = SourceLocationConverter(file: url.path, tree: syntax)
115-
let diagnostics = ParseDiagnosticsGenerator.diagnostics(for: syntax)
116-
for diagnostic in diagnostics {
117-
let location = diagnostic.location(converter: expectedConverter)
118-
parsingDiagnosticHandler(diagnostic, location)
119-
}
120-
}
121-
122141
let context = Context(
123-
configuration: configuration, findingConsumer: findingConsumer, fileURL: url,
124-
sourceFileSyntax: syntax, source: source, ruleNameCache: ruleNameCache)
142+
configuration: configuration, operatorTable: operatorTable, findingConsumer: findingConsumer,
143+
fileURL: url, sourceFileSyntax: syntax, source: source, ruleNameCache: ruleNameCache)
125144
let pipeline = LintPipeline(context: context)
126145
pipeline.walk(Syntax(syntax))
127146

@@ -131,10 +150,8 @@ public final class SwiftLinter {
131150

132151
// Perform whitespace linting by comparing the input source text with the output of the
133152
// pretty-printer.
134-
let operatorContext = OperatorContext.makeBuiltinOperatorContext()
135153
let printer = PrettyPrinter(
136154
context: context,
137-
operatorContext: operatorContext,
138155
node: Syntax(syntax),
139156
printTokenStream: debugOptions.contains(.dumpTokenStream),
140157
whitespaceOnly: true)

0 commit comments

Comments
 (0)