Skip to content

Commit ff63b29

Browse files
authored
Merge pull request #401 from allevato/operator-folding
Use the `SwiftOperators` module to fold sequence expressions.
2 parents 475313f + 15959b2 commit ff63b29

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)