Skip to content

Commit cd992a8

Browse files
committed
Add --enable-experimental-feature to enable those features in the parser.
Also add a couple small tests for value generics to exercise the capability in tests. Fixes #875.
1 parent 637cb85 commit cd992a8

File tree

11 files changed

+167
-31
lines changed

11 files changed

+167
-31
lines changed

Sources/SwiftFormat/API/SwiftFormatError.swift

+18-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,20 @@ 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
}

Sources/SwiftFormat/API/SwiftFormatter.swift

+5
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ 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. The names of these features correspond to the names of the
94+
/// `Parser.ExperimentalFeatures` enum in the `SwiftParser` module of swift-syntax.
9295
/// - outputStream: A value conforming to `TextOutputStream` to which the formatted output will
9396
/// be written.
9497
/// - parsingDiagnosticHandler: An optional callback that will be notified if there are any
@@ -98,6 +101,7 @@ public final class SwiftFormatter {
98101
source: String,
99102
assumingFileURL url: URL?,
100103
selection: Selection,
104+
experimentalFeatures: Set<String> = [],
101105
to outputStream: inout Output,
102106
parsingDiagnosticHandler: ((Diagnostic, SourceLocation) -> Void)? = nil
103107
) throws {
@@ -110,6 +114,7 @@ public final class SwiftFormatter {
110114
source: source,
111115
operatorTable: .standardOperators,
112116
assumingFileURL: url,
117+
experimentalFeatures: experimentalFeatures,
113118
parsingDiagnosticHandler: parsingDiagnosticHandler
114119
)
115120
try format(

Sources/SwiftFormat/API/SwiftLinter.swift

+5
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,16 @@ 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. The names of these features correspond to the names of the
86+
/// `Parser.ExperimentalFeatures` enum in the `SwiftParser` module of swift-syntax.
8487
/// - parsingDiagnosticHandler: An optional callback that will be notified if there are any
8588
/// errors when parsing the source code.
8689
/// - Throws: If an unrecoverable error occurs when formatting the code.
8790
public func lint(
8891
source: String,
8992
assumingFileURL url: URL,
93+
experimentalFeatures: Set<String> = [],
9094
parsingDiagnosticHandler: ((Diagnostic, SourceLocation) -> Void)? = nil
9195
) throws {
9296
// If the file or input string is completely empty, do nothing. This prevents even a trailing
@@ -98,6 +102,7 @@ public final class SwiftLinter {
98102
source: source,
99103
operatorTable: .standardOperators,
100104
assumingFileURL: url,
105+
experimentalFeatures: experimentalFeatures,
101106
parsingDiagnosticHandler: parsingDiagnosticHandler
102107
)
103108
try lint(

Sources/SwiftFormat/Core/Parsing.swift

+18-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,35 @@ 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+
/// The names of these features correspond to the names of the `Parser.ExperimentalFeatures`
34+
/// enum in the `SwiftParser` module of swift-syntax.
3235
/// - parsingDiagnosticHandler: An optional callback that will be notified if there are any
3336
/// errors when parsing the source code.
3437
/// - Throws: If an unrecoverable error occurs when formatting the code.
3538
func parseAndEmitDiagnostics(
3639
source: String,
3740
operatorTable: OperatorTable,
3841
assumingFileURL url: URL?,
42+
experimentalFeatures: Set<String>,
3943
parsingDiagnosticHandler: ((Diagnostic, SourceLocation) -> Void)? = nil
4044
) throws -> SourceFileSyntax {
41-
let sourceFile =
42-
operatorTable.foldAll(Parser.parse(source: source)) { _ in }.as(SourceFileSyntax.self)!
43-
45+
var experimentalFeaturesSet: Parser.ExperimentalFeatures = []
46+
for featureName in experimentalFeatures {
47+
guard let featureValue = Parser.ExperimentalFeatures(name: featureName) else {
48+
throw SwiftFormatError.unrecognizedExperimentalFeature(featureName)
49+
}
50+
experimentalFeaturesSet.formUnion(featureValue)
51+
}
52+
var source = source
53+
let sourceFile = source.withUTF8 { sourceBytes in
54+
operatorTable.foldAll(Parser.parse(source: sourceBytes, experimentalFeatures: experimentalFeaturesSet)) { _ in }
55+
.as(SourceFileSyntax.self)!
56+
}
4457
let diagnostics = ParseDiagnosticsGenerator.diagnostics(for: sourceFile)
4558
var hasErrors = false
4659
if let parsingDiagnosticHandler = parsingDiagnosticHandler {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
/// Parses the given source string and returns the corresponding `SourceFileSyntax` node.
18+
///
19+
/// - Parameters:
20+
/// - source: The source text to parse.
21+
/// - experimentalFeatures: The set of experimental features that should be enabled in the parser.
22+
@_spi(Testing)
23+
public func parseForTesting(
24+
source: String,
25+
experimentalFeatures: Parser.ExperimentalFeatures
26+
) -> SourceFileSyntax {
27+
var source = source
28+
return source.withUTF8 { sourceBytes in
29+
Parser.parse(
30+
source: sourceBytes,
31+
experimentalFeatures: experimentalFeatures
32+
)
33+
}
34+
}

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+
parseForTesting(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)