Skip to content

Commit 2830399

Browse files
authored
Merge pull request #876 from allevato/experimental-features
Add `--enable-experimental-feature` to enable those features in the parser.
2 parents 0c05520 + fee42c9 commit 2830399

File tree

11 files changed

+176
-31
lines changed

11 files changed

+176
-31
lines changed

Sources/SwiftFormat/API/SwiftFormatError.swift

+21-1
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import Foundation
1314
import SwiftSyntax
1415

1516
/// Errors that can be thrown by the `SwiftFormatter` and `SwiftLinter` APIs.
16-
public enum SwiftFormatError: Error {
17+
public enum SwiftFormatError: LocalizedError {
1718

1819
/// The requested file was not readable or it did not exist.
1920
case fileNotReadable
@@ -23,4 +24,23 @@ public enum SwiftFormatError: Error {
2324

2425
/// The file contains invalid or unrecognized Swift syntax and cannot be handled safely.
2526
case fileContainsInvalidSyntax
27+
28+
/// The requested experimental feature name was not recognized by the parser.
29+
case unrecognizedExperimentalFeature(String)
30+
31+
public var errorDescription: String? {
32+
switch self {
33+
case .fileNotReadable:
34+
return "file is not readable or does not exist"
35+
case .isDirectory:
36+
return "requested path is a directory, not a file"
37+
case .fileContainsInvalidSyntax:
38+
return "file contains invalid Swift syntax"
39+
case .unrecognizedExperimentalFeature(let name):
40+
return "experimental feature '\(name)' was not recognized by the Swift parser"
41+
}
42+
}
2643
}
44+
45+
extension SwiftFormatError: Equatable {}
46+
extension SwiftFormatError: Hashable {}

Sources/SwiftFormat/API/SwiftFormatter.swift

+6
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ public final class SwiftFormatter {
8989
/// which is associated with any diagnostics emitted during formatting. If this is nil, a
9090
/// dummy value will be used.
9191
/// - selection: The ranges to format
92+
/// - experimentalFeatures: The set of experimental features that should be enabled in the
93+
/// parser. These names must be from the set of parser-recognized experimental language
94+
/// features in `SwiftParser`'s `Parser.ExperimentalFeatures` enum, which match the spelling
95+
/// defined in the compiler's `Features.def` file.
9296
/// - outputStream: A value conforming to `TextOutputStream` to which the formatted output will
9397
/// be written.
9498
/// - parsingDiagnosticHandler: An optional callback that will be notified if there are any
@@ -98,6 +102,7 @@ public final class SwiftFormatter {
98102
source: String,
99103
assumingFileURL url: URL?,
100104
selection: Selection,
105+
experimentalFeatures: Set<String> = [],
101106
to outputStream: inout Output,
102107
parsingDiagnosticHandler: ((Diagnostic, SourceLocation) -> Void)? = nil
103108
) throws {
@@ -110,6 +115,7 @@ public final class SwiftFormatter {
110115
source: source,
111116
operatorTable: .standardOperators,
112117
assumingFileURL: url,
118+
experimentalFeatures: experimentalFeatures,
113119
parsingDiagnosticHandler: parsingDiagnosticHandler
114120
)
115121
try format(

Sources/SwiftFormat/API/SwiftLinter.swift

+6
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,17 @@ public final class SwiftLinter {
8181
/// - Parameters:
8282
/// - source: The Swift source code to be linted.
8383
/// - url: A file URL denoting the filename/path that should be assumed for this source code.
84+
/// - experimentalFeatures: The set of experimental features that should be enabled in the
85+
/// parser. These names must be from the set of parser-recognized experimental language
86+
/// features in `SwiftParser`'s `Parser.ExperimentalFeatures` enum, which match the spelling
87+
/// defined in the compiler's `Features.def` file.
8488
/// - parsingDiagnosticHandler: An optional callback that will be notified if there are any
8589
/// errors when parsing the source code.
8690
/// - Throws: If an unrecoverable error occurs when formatting the code.
8791
public func lint(
8892
source: String,
8993
assumingFileURL url: URL,
94+
experimentalFeatures: Set<String> = [],
9095
parsingDiagnosticHandler: ((Diagnostic, SourceLocation) -> Void)? = nil
9196
) throws {
9297
// If the file or input string is completely empty, do nothing. This prevents even a trailing
@@ -98,6 +103,7 @@ public final class SwiftLinter {
98103
source: source,
99104
operatorTable: .standardOperators,
100105
assumingFileURL: url,
106+
experimentalFeatures: experimentalFeatures,
101107
parsingDiagnosticHandler: parsingDiagnosticHandler
102108
)
103109
try lint(

Sources/SwiftFormat/Core/Parsing.swift

+19-5
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import Foundation
1414
import SwiftDiagnostics
1515
import SwiftOperators
16-
import SwiftParser
16+
@_spi(ExperimentalLanguageFeatures) import SwiftParser
1717
import SwiftParserDiagnostics
1818
import SwiftSyntax
1919

@@ -25,22 +25,36 @@ import SwiftSyntax
2525
///
2626
/// - Parameters:
2727
/// - source: The Swift source code to be formatted.
28+
/// - operatorTable: The operator table to use for sequence folding.
2829
/// - url: A file URL denoting the filename/path that should be assumed for this syntax tree,
2930
/// which is associated with any diagnostics emitted during formatting. If this is nil, a
3031
/// dummy value will be used.
31-
/// - operatorTable: The operator table to use for sequence folding.
32+
/// - experimentalFeatures: The set of experimental features that should be enabled in the parser.
33+
/// These names must be from the set of parser-recognized experimental language features in
34+
/// `SwiftParser`'s `Parser.ExperimentalFeatures` enum, which match the spelling defined in the
35+
/// compiler's `Features.def` file.
3236
/// - parsingDiagnosticHandler: An optional callback that will be notified if there are any
3337
/// errors when parsing the source code.
3438
/// - Throws: If an unrecoverable error occurs when formatting the code.
3539
func parseAndEmitDiagnostics(
3640
source: String,
3741
operatorTable: OperatorTable,
3842
assumingFileURL url: URL?,
43+
experimentalFeatures: Set<String>,
3944
parsingDiagnosticHandler: ((Diagnostic, SourceLocation) -> Void)? = nil
4045
) throws -> SourceFileSyntax {
41-
let sourceFile =
42-
operatorTable.foldAll(Parser.parse(source: source)) { _ in }.as(SourceFileSyntax.self)!
43-
46+
var experimentalFeaturesSet: Parser.ExperimentalFeatures = []
47+
for featureName in experimentalFeatures {
48+
guard let featureValue = Parser.ExperimentalFeatures(name: featureName) else {
49+
throw SwiftFormatError.unrecognizedExperimentalFeature(featureName)
50+
}
51+
experimentalFeaturesSet.formUnion(featureValue)
52+
}
53+
var source = source
54+
let sourceFile = source.withUTF8 { sourceBytes in
55+
operatorTable.foldAll(Parser.parse(source: sourceBytes, experimentalFeatures: experimentalFeaturesSet)) { _ in }
56+
.as(SourceFileSyntax.self)!
57+
}
4458
let diagnostics = ParseDiagnosticsGenerator.diagnostics(for: sourceFile)
4559
var hasErrors = false
4660
if let parsingDiagnosticHandler = parsingDiagnosticHandler {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 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+
@_spi(ExperimentalLanguageFeatures) import SwiftParser
14+
import SwiftSyntax
15+
import XCTest
16+
17+
extension Parser {
18+
/// Parses the given source string and returns the corresponding `SourceFileSyntax` node.
19+
///
20+
/// - Parameters:
21+
/// - source: The source text to parse.
22+
/// - experimentalFeatures: The set of experimental features that should be enabled in the
23+
/// parser.
24+
@_spi(Testing)
25+
public static func parse(
26+
source: String,
27+
experimentalFeatures: Parser.ExperimentalFeatures
28+
) -> SourceFileSyntax {
29+
var source = source
30+
return source.withUTF8 { sourceBytes in
31+
parse(
32+
source: sourceBytes,
33+
experimentalFeatures: experimentalFeatures
34+
)
35+
}
36+
}
37+
}

Sources/swift-format/Frontend/FormatFrontend.swift

+5-8
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class FormatFrontend: Frontend {
5656
source: source,
5757
assumingFileURL: url,
5858
selection: fileToProcess.selection,
59+
experimentalFeatures: Set(lintFormatOptions.experimentalFeatures),
5960
to: &buffer,
6061
parsingDiagnosticHandler: diagnosticHandler
6162
)
@@ -69,15 +70,11 @@ class FormatFrontend: Frontend {
6970
source: source,
7071
assumingFileURL: url,
7172
selection: fileToProcess.selection,
73+
experimentalFeatures: Set(lintFormatOptions.experimentalFeatures),
7274
to: &stdoutStream,
7375
parsingDiagnosticHandler: diagnosticHandler
7476
)
7577
}
76-
} catch SwiftFormatError.fileNotReadable {
77-
diagnosticsEngine.emitError(
78-
"Unable to format \(url.relativePath): file is not readable or does not exist."
79-
)
80-
return
8178
} catch SwiftFormatError.fileContainsInvalidSyntax {
8279
guard !lintFormatOptions.ignoreUnparsableFiles else {
8380
guard !inPlace else {
@@ -87,10 +84,10 @@ class FormatFrontend: Frontend {
8784
stdoutStream.write(source)
8885
return
8986
}
90-
// Otherwise, relevant diagnostics about the problematic nodes have been emitted.
91-
return
87+
// Otherwise, relevant diagnostics about the problematic nodes have already been emitted; we
88+
// don't need to print anything else.
9289
} catch {
93-
diagnosticsEngine.emitError("Unable to format \(url.relativePath): \(error)")
90+
diagnosticsEngine.emitError("Unable to format \(url.relativePath): \(error.localizedDescription).")
9491
}
9592
}
9693
}

Sources/swift-format/Frontend/LintFrontend.swift

+5-11
Original file line numberDiff line numberDiff line change
@@ -35,30 +35,24 @@ class LintFrontend: Frontend {
3535
do {
3636
try linter.lint(
3737
source: source,
38-
assumingFileURL: url
38+
assumingFileURL: url,
39+
experimentalFeatures: Set(lintFormatOptions.experimentalFeatures)
3940
) { (diagnostic, location) in
4041
guard !self.lintFormatOptions.ignoreUnparsableFiles else {
4142
// No diagnostics should be emitted in this mode.
4243
return
4344
}
4445
self.diagnosticsEngine.consumeParserDiagnostic(diagnostic, location)
4546
}
46-
47-
} catch SwiftFormatError.fileNotReadable {
48-
diagnosticsEngine.emitError(
49-
"Unable to lint \(url.relativePath): file is not readable or does not exist."
50-
)
51-
return
5247
} catch SwiftFormatError.fileContainsInvalidSyntax {
5348
guard !lintFormatOptions.ignoreUnparsableFiles else {
5449
// The caller wants to silently ignore this error.
5550
return
5651
}
57-
// Otherwise, relevant diagnostics about the problematic nodes have been emitted.
58-
return
52+
// Otherwise, relevant diagnostics about the problematic nodes have already been emitted; we
53+
// don't need to print anything else.
5954
} catch {
60-
diagnosticsEngine.emitError("Unable to lint \(url.relativePath): \(error)")
61-
return
55+
diagnosticsEngine.emitError("Unable to lint \(url.relativePath): \(error.localizedDescription).")
6256
}
6357
}
6458
}

Sources/swift-format/Subcommands/LintFormatOptions.swift

+9
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,15 @@ struct LintFormatOptions: ParsableArguments {
9898
)
9999
var followSymlinks: Bool = false
100100

101+
@Option(
102+
name: .customLong("enable-experimental-feature"),
103+
help: """
104+
The name of an experimental swift-syntax parser feature that should be enabled by \
105+
swift-format. Multiple features can be enabled by specifying this flag multiple times.
106+
"""
107+
)
108+
var experimentalFeatures: [String] = []
109+
101110
/// The list of paths to Swift source files that should be formatted or linted.
102111
@Argument(help: "Zero or more input filenames.")
103112
var paths: [String] = []

Tests/SwiftFormatTests/PrettyPrint/PrettyPrintTestCase.swift

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import SwiftFormat
22
@_spi(Rules) @_spi(Testing) import SwiftFormat
33
import SwiftOperators
4-
import SwiftParser
4+
@_spi(ExperimentalLanguageFeatures) import SwiftParser
55
import SwiftSyntax
66
import XCTest
77
@_spi(Testing) import _SwiftFormatTestSupport
@@ -18,6 +18,8 @@ class PrettyPrintTestCase: DiagnosingTestCase {
1818
/// changes that insert or remove non-whitespace characters (like trailing commas).
1919
/// - findings: A list of `FindingSpec` values that describe the findings that are expected to
2020
/// be emitted. These are currently only checked if `whitespaceOnly` is true.
21+
/// - experimentalFeatures: The set of experimental features that should be enabled in the
22+
/// parser.
2123
/// - file: The file in which failure occurred. Defaults to the file name of the test case in
2224
/// which this function was called.
2325
/// - line: The line number on which failure occurred. Defaults to the line number on which this
@@ -29,6 +31,7 @@ class PrettyPrintTestCase: DiagnosingTestCase {
2931
configuration: Configuration = Configuration.forTesting,
3032
whitespaceOnly: Bool = false,
3133
findings: [FindingSpec] = [],
34+
experimentalFeatures: Parser.ExperimentalFeatures = [],
3235
file: StaticString = #file,
3336
line: UInt = #line
3437
) {
@@ -44,6 +47,7 @@ class PrettyPrintTestCase: DiagnosingTestCase {
4447
configuration: configuration,
4548
selection: markedInput.selection,
4649
whitespaceOnly: whitespaceOnly,
50+
experimentalFeatures: experimentalFeatures,
4751
findingConsumer: { emittedFindings.append($0) }
4852
)
4953
assertStringsEqualWithDiff(
@@ -76,6 +80,7 @@ class PrettyPrintTestCase: DiagnosingTestCase {
7680
configuration: configuration,
7781
selection: markedInput.selection,
7882
whitespaceOnly: whitespaceOnly,
83+
experimentalFeatures: experimentalFeatures,
7984
findingConsumer: { _ in } // Ignore findings during the idempotence check.
8085
)
8186
assertStringsEqualWithDiff(
@@ -95,18 +100,23 @@ class PrettyPrintTestCase: DiagnosingTestCase {
95100
/// - configuration: The formatter configuration.
96101
/// - whitespaceOnly: If true, the pretty printer should only apply whitespace changes and omit
97102
/// changes that insert or remove non-whitespace characters (like trailing commas).
103+
/// - experimentalFeatures: The set of experimental features that should be enabled in the
104+
/// parser.
98105
/// - findingConsumer: A function called for each finding that is emitted by the pretty printer.
99106
/// - Returns: The pretty-printed text, or nil if an error occurred and a test failure was logged.
100107
private func prettyPrintedSource(
101108
_ source: String,
102109
configuration: Configuration,
103110
selection: Selection,
104111
whitespaceOnly: Bool,
112+
experimentalFeatures: Parser.ExperimentalFeatures = [],
105113
findingConsumer: @escaping (Finding) -> Void
106114
) -> (String, Context) {
107115
// Ignore folding errors for unrecognized operators so that we fallback to a reasonable default.
108116
let sourceFileSyntax =
109-
OperatorTable.standardOperators.foldAll(Parser.parse(source: source)) { _ in }
117+
OperatorTable.standardOperators.foldAll(
118+
Parser.parse(source: source, experimentalFeatures: experimentalFeatures)
119+
) { _ in }
110120
.as(SourceFileSyntax.self)!
111121
let context = makeContext(
112122
sourceFileSyntax: sourceFileSyntax,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
@_spi(ExperimentalLanguageFeatures) import SwiftParser
2+
3+
final class ValueGenericsTests: PrettyPrintTestCase {
4+
func testValueGenericDeclaration() {
5+
let input = "struct Foo<let n: Int> { static let bar = n }"
6+
let expected = """
7+
struct Foo<
8+
let n: Int
9+
> {
10+
static let bar = n
11+
}
12+
13+
"""
14+
assertPrettyPrintEqual(
15+
input: input,
16+
expected: expected,
17+
linelength: 20,
18+
experimentalFeatures: [.valueGenerics]
19+
)
20+
}
21+
22+
func testValueGenericTypeUsage() {
23+
let input =
24+
"""
25+
let v1: Vector<100, Int>
26+
let v2 = Vector<100, Int>()
27+
"""
28+
let expected = """
29+
let v1:
30+
Vector<
31+
100, Int
32+
>
33+
let v2 =
34+
Vector<
35+
100, Int
36+
>()
37+
38+
"""
39+
assertPrettyPrintEqual(
40+
input: input,
41+
expected: expected,
42+
linelength: 15,
43+
experimentalFeatures: [.valueGenerics]
44+
)
45+
}
46+
}

0 commit comments

Comments
 (0)