Skip to content

Commit a008511

Browse files
committed
Support construction of DeclSyntax subtypes from string interpolation
Allow the creation of e.g. an `ExtensionDeclSyntax` as ```swift let extension = try ExtensionDeclSyntax("extension Foo: Proto {}") ``` The initializer throws if the string describes a declaration other than an 'extension'. If the string can get parsed as an extension but contains syntax errors, we return a node that contains syntax errors. This matches the behavior of the `HasTrailingCodeBlock` intializers that take a header + result builder.
1 parent e42bece commit a008511

File tree

6 files changed

+120
-26
lines changed

6 files changed

+120
-26
lines changed

Sources/SwiftSyntaxBuilder/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
add_swift_host_library(SwiftSyntaxBuilder
1010
ConvenienceInitializers.swift
11+
DeclSyntaxParseable.swift
1112
Indenter.swift
1213
ResultBuilderExtensions.swift
1314
SwiftSyntaxBuilderCompatibility.swift
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 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 SwiftSyntax
14+
15+
/// Adds an initializer that allows the creation of declaration from string
16+
/// interpolations.
17+
///
18+
/// - Warning: This protocol is considered an implementation detail. Do not rely
19+
/// on it, only the initializer that it adds.
20+
public protocol DeclSyntaxParseable: DeclSyntaxProtocol {}
21+
public extension DeclSyntaxParseable {
22+
/// Create a syntax node from the given string interpolation.
23+
///
24+
/// This initializer throws if the syntax node was not able to be parsed as
25+
/// this type, e.g. when calling `ClassDeclSyntax("actor Foo {})`.
26+
///
27+
/// If there are syntax errors in the string, the initializer will return a
28+
/// node that contains errors without throwing.
29+
init(_ stringInterpolation: SyntaxNodeString) throws {
30+
let node: DeclSyntax = "\(stringInterpolation)"
31+
if let castedDecl = node.as(Self.self) {
32+
self = castedDecl
33+
} else {
34+
throw SyntaxStringInterpolationError.producedInvalidNodeType(expectedType: Self.self, actualNode: node)
35+
}
36+
}
37+
}
38+
39+
// These are all the declarations that get parsed from `parseDecl`
40+
extension ActorDeclSyntax: DeclSyntaxParseable {}
41+
extension AssociatedTypeDeclSyntax: DeclSyntaxParseable {}
42+
extension EnumCaseDeclSyntax: DeclSyntaxParseable {}
43+
extension ClassDeclSyntax: DeclSyntaxParseable {}
44+
extension DeinitializerDeclSyntax: DeclSyntaxParseable {}
45+
extension EnumDeclSyntax: DeclSyntaxParseable {}
46+
extension ExtensionDeclSyntax: DeclSyntaxParseable {}
47+
extension FunctionDeclSyntax: DeclSyntaxParseable {}
48+
extension ImportDeclSyntax: DeclSyntaxParseable {}
49+
extension VariableDeclSyntax: DeclSyntaxParseable {}
50+
extension MacroDeclSyntax: DeclSyntaxParseable {}
51+
extension OperatorDeclSyntax: DeclSyntaxParseable {}
52+
extension MacroExpansionDeclSyntax: DeclSyntaxParseable {}
53+
extension PrecedenceGroupDeclSyntax: DeclSyntaxParseable {}
54+
extension ProtocolDeclSyntax: DeclSyntaxParseable {}
55+
extension StructDeclSyntax: DeclSyntaxParseable {}
56+
extension SubscriptDeclSyntax: DeclSyntaxParseable {}
57+
extension TypeAliasDeclSyntax: DeclSyntaxParseable {}

Sources/SwiftSyntaxBuilder/SyntaxNodeWithBody.swift

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,18 @@ import SwiftSyntax
1414

1515
// MARK: - PartialSyntaxNode
1616

17+
@available(*, deprecated, renamed: "SyntaxNodeString")
18+
public typealias PartialSyntaxNodeString = SyntaxNodeString
19+
1720
/// A type that is expressible by string interpolation the same way that syntax
1821
/// nodes are, but instead of producing a node, it stores the string interpolation
19-
/// text. Used to represent partial syntax nodes in initializers that take a
22+
/// text.
23+
///
24+
/// Used to represent partial syntax nodes in initializers that take a
2025
/// trailing code block.
2126
///
2227
/// This type should always be constructed using string interpolation.
23-
public struct PartialSyntaxNodeString: SyntaxExpressibleByStringInterpolation {
28+
public struct SyntaxNodeString: SyntaxExpressibleByStringInterpolation {
2429
let sourceText: [UInt8]
2530

2631
public init(stringInterpolation: SyntaxStringInterpolation) {
@@ -29,7 +34,7 @@ public struct PartialSyntaxNodeString: SyntaxExpressibleByStringInterpolation {
2934
}
3035

3136
extension SyntaxStringInterpolation {
32-
public mutating func appendInterpolation(_ value: PartialSyntaxNodeString) {
37+
public mutating func appendInterpolation(_ value: SyntaxNodeString) {
3338
sourceText.append(contentsOf: value.sourceText)
3439
self.lastIndentation = nil
3540
}
@@ -59,11 +64,11 @@ public protocol HasTrailingCodeBlock {
5964
/// ```
6065
///
6166
/// Throws an error if `header` defines a different node type than the type the initializer is called on. E.g. if calling `try IfStmtSyntax("while x < 5") {}`
62-
init(_ header: PartialSyntaxNodeString, @CodeBlockItemListBuilder bodyBuilder: () throws -> CodeBlockItemListSyntax) rethrows
67+
init(_ header: SyntaxNodeString, @CodeBlockItemListBuilder bodyBuilder: () throws -> CodeBlockItemListSyntax) rethrows
6368
}
6469

6570
public extension HasTrailingCodeBlock where Self: StmtSyntaxProtocol {
66-
init(_ header: PartialSyntaxNodeString, @CodeBlockItemListBuilder bodyBuilder: () throws -> CodeBlockItemListSyntax) throws {
71+
init(_ header: SyntaxNodeString, @CodeBlockItemListBuilder bodyBuilder: () throws -> CodeBlockItemListSyntax) throws {
6772
let stmt = StmtSyntax("\(header) {}")
6873
guard let castedStmt = stmt.as(Self.self) else {
6974
throw SyntaxStringInterpolationError.producedInvalidNodeType(expectedType: Self.self, actualNode: stmt)
@@ -74,7 +79,7 @@ public extension HasTrailingCodeBlock where Self: StmtSyntaxProtocol {
7479
}
7580

7681
extension CatchClauseSyntax: HasTrailingCodeBlock {
77-
public init(_ header: PartialSyntaxNodeString, @CodeBlockItemListBuilder bodyBuilder: () throws -> CodeBlockItemListSyntax) rethrows {
82+
public init(_ header: SyntaxNodeString, @CodeBlockItemListBuilder bodyBuilder: () throws -> CodeBlockItemListSyntax) rethrows {
7883
self = CatchClauseSyntax("\(header) {}")
7984
self.body = try CodeBlockSyntax(statements: bodyBuilder())
8085
}
@@ -109,11 +114,11 @@ public protocol HasTrailingOptionalCodeBlock {
109114
/// ```
110115
///
111116
/// Throws an error if `header` defines a different node type than the type the initializer is called on. E.g. if calling `try FunctionDeclSyntax("init") {}`
112-
init(_ header: PartialSyntaxNodeString, @CodeBlockItemListBuilder bodyBuilder: () throws -> CodeBlockItemListSyntax) throws
117+
init(_ header: SyntaxNodeString, @CodeBlockItemListBuilder bodyBuilder: () throws -> CodeBlockItemListSyntax) throws
113118
}
114119

115120
public extension HasTrailingOptionalCodeBlock where Self: DeclSyntaxProtocol {
116-
init(_ header: PartialSyntaxNodeString, @CodeBlockItemListBuilder bodyBuilder: () throws -> CodeBlockItemListSyntax) throws {
121+
init(_ header: SyntaxNodeString, @CodeBlockItemListBuilder bodyBuilder: () throws -> CodeBlockItemListSyntax) throws {
117122
let decl = DeclSyntax("\(header) {}")
118123
guard let castedDecl = decl.as(Self.self) else {
119124
throw SyntaxStringInterpolationError.producedInvalidNodeType(expectedType: Self.self, actualNode: decl)
@@ -154,11 +159,11 @@ public protocol HasTrailingMemberDeclBlock {
154159
/// ```
155160
///
156161
/// Throws an error if `header` defines a different node type than the type the initializer is called on. E.g. if calling `try StructDeclSyntax("class MyClass") {}`
157-
init(_ header: PartialSyntaxNodeString, @MemberBlockItemListBuilder membersBuilder: () throws -> MemberBlockItemListSyntax) throws
162+
init(_ header: SyntaxNodeString, @MemberBlockItemListBuilder membersBuilder: () throws -> MemberBlockItemListSyntax) throws
158163
}
159164

160165
public extension HasTrailingMemberDeclBlock where Self: DeclSyntaxProtocol {
161-
init(_ header: PartialSyntaxNodeString, @MemberBlockItemListBuilder membersBuilder: () throws -> MemberBlockItemListSyntax) throws {
166+
init(_ header: SyntaxNodeString, @MemberBlockItemListBuilder membersBuilder: () throws -> MemberBlockItemListSyntax) throws {
162167
let decl = DeclSyntax("\(header) {}")
163168
guard let castedDecl = decl.as(Self.self) else {
164169
throw SyntaxStringInterpolationError.producedInvalidNodeType(expectedType: Self.self, actualNode: decl)
@@ -198,7 +203,7 @@ public extension IfExprSyntax {
198203
///
199204
/// Throws an error if `header` does not start an `if` expression. E.g. if calling `try IfExprSyntax("while true") {}`
200205
init(
201-
_ header: PartialSyntaxNodeString,
206+
_ header: SyntaxNodeString,
202207
@CodeBlockItemListBuilder bodyBuilder: () throws -> CodeBlockItemListSyntax,
203208
@CodeBlockItemListBuilder `else` elseBuilder: () throws -> CodeBlockItemListSyntax? = { nil }
204209
) throws {
@@ -246,7 +251,7 @@ public extension IfExprSyntax {
246251
/// ```
247252
///
248253
/// Throws an error if `header` does not start an `if` expression. E.g. if calling `try IfExprSyntax("while true", bodyBuilder: {}, elseIf: {})`
249-
init(_ header: PartialSyntaxNodeString, @CodeBlockItemListBuilder bodyBuilder: () throws -> CodeBlockItemListSyntax, elseIf: IfExprSyntax) throws {
254+
init(_ header: SyntaxNodeString, @CodeBlockItemListBuilder bodyBuilder: () throws -> CodeBlockItemListSyntax, elseIf: IfExprSyntax) throws {
250255
let expr = ExprSyntax("\(header) {}")
251256
guard let ifExpr = expr.as(Self.self) else {
252257
throw SyntaxStringInterpolationError.producedInvalidNodeType(expectedType: Self.self, actualNode: expr)
@@ -279,7 +284,7 @@ extension SwitchCaseSyntax {
279284
/// ```
280285
///
281286
/// Throws an error if `header` does not start a switch case item. E.g. if calling `try SwitchCaseSyntax("func foo") {}`
282-
public init(_ header: PartialSyntaxNodeString, @CodeBlockItemListBuilder statementsBuilder: () throws -> CodeBlockItemListSyntax) rethrows {
287+
public init(_ header: SyntaxNodeString, @CodeBlockItemListBuilder statementsBuilder: () throws -> CodeBlockItemListSyntax) rethrows {
283288
self = SwitchCaseSyntax("\(header)")
284289
self.statements = try statementsBuilder()
285290
}
@@ -313,7 +318,7 @@ public extension SwitchExprSyntax {
313318
/// ```
314319
///
315320
/// Throws an error if `header` does not start a switch expression. E.g. if calling `try SwitchExprSyntax("if x < 42") {}`
316-
init(_ header: PartialSyntaxNodeString, @SwitchCaseListBuilder casesBuilder: () throws -> SwitchCaseListSyntax = { SwitchCaseListSyntax([]) }) throws {
321+
init(_ header: SyntaxNodeString, @SwitchCaseListBuilder casesBuilder: () throws -> SwitchCaseListSyntax = { SwitchCaseListSyntax([]) }) throws {
317322
let expr = ExprSyntax("\(header) {}")
318323
guard let switchExpr = expr.as(Self.self) else {
319324
throw SyntaxStringInterpolationError.producedInvalidNodeType(expectedType: Self.self, actualNode: expr)
@@ -347,7 +352,7 @@ public extension VariableDeclSyntax {
347352
/// ```
348353
///
349354
/// Throws an error if `header` does not start a variable declaration. E.g. if calling `try VariableDeclSyntax("func foo") {}`
350-
init(_ header: PartialSyntaxNodeString, @CodeBlockItemListBuilder accessor: () throws -> CodeBlockItemListSyntax) throws {
355+
init(_ header: SyntaxNodeString, @CodeBlockItemListBuilder accessor: () throws -> CodeBlockItemListSyntax) throws {
351356
let decl = DeclSyntax("\(header) {}")
352357
guard let castedDecl = decl.as(Self.self) else {
353358
throw SyntaxStringInterpolationError.producedInvalidNodeType(expectedType: Self.self, actualNode: decl)

Sources/SwiftSyntaxBuilder/SyntaxParsable+ExpressibleByStringInterpolation.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,31 @@ import SwiftParserDiagnostics
2020
import OSLog
2121
#endif
2222

23+
fileprivate var suppressStringInterpolationParsingErrors = false
24+
25+
/// Run the body, disabling any runtime warnings about syntax error in string
26+
/// interpolation inside the body.
27+
///
28+
/// Used to test the behavior of string interpolation with syntax errors.
29+
@_spi(Testing)
30+
public func withStringInterpolationParsingErrorsSuppressed<T>(_ body: () throws -> T) rethrows -> T {
31+
suppressStringInterpolationParsingErrors = true
32+
defer {
33+
suppressStringInterpolationParsingErrors = false
34+
}
35+
return try body()
36+
}
37+
2338
extension SyntaxParseable {
2439
public typealias StringInterpolation = SyntaxStringInterpolation
2540

2641
/// Assuming that this node contains a syntax error, log it using OSLog if we
2742
/// are on a platform that supports OSLog, otherwise don't do anything.
2843
private func logStringInterpolationParsingError() {
2944
#if canImport(OSLog) && !SWIFTSYNTAX_NO_OSLOG_DEPENDENCY
30-
if #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, macCatalyst 14.0, *) {
45+
if #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, macCatalyst 14.0, *),
46+
!suppressStringInterpolationParsingErrors
47+
{
3148
let diagnostics = ParseDiagnosticsGenerator.diagnostics(for: self)
3249
let formattedDiagnostics = DiagnosticsFormatter().annotatedSource(tree: self, diags: diagnostics)
3350
Logger(subsystem: "SwiftSyntax", category: "ParseError").fault(

Tests/SwiftBasicFormatTest/BasicFormatTests.swift

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import SwiftBasicFormat
1414
import SwiftParser
15-
import SwiftSyntaxBuilder
15+
@_spi(Testing) import SwiftSyntaxBuilder
1616
import SwiftSyntax
1717

1818
import XCTest
@@ -491,15 +491,17 @@ final class BasicFormatTest: XCTestCase {
491491
}
492492

493493
func testUnexpectedIsNotFormatted() {
494-
let expr: ExprSyntax = """
495-
let foo=1
496-
"""
497-
498-
assertFormatted(
499-
tree: expr,
500-
expected: """
494+
withStringInterpolationParsingErrorsSuppressed {
495+
let expr: ExprSyntax = """
501496
let foo=1
502497
"""
503-
)
498+
499+
assertFormatted(
500+
tree: expr,
501+
expected: """
502+
let foo=1
503+
"""
504+
)
505+
}
504506
}
505507
}

Tests/SwiftSyntaxBuilderTest/StringInterpolationTests.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import _SwiftSyntaxTestSupport
1414
import SwiftSyntax
15-
import SwiftSyntaxBuilder
15+
@_spi(Testing) import SwiftSyntaxBuilder
1616
import SwiftParser
1717
import SwiftBasicFormat
1818

@@ -500,4 +500,16 @@ final class StringInterpolationTests: XCTestCase {
500500
)
501501
}
502502
}
503+
504+
func testExtensionDeclFromStringInterpolation() throws {
505+
let extensionDecl = try ExtensionDeclSyntax("extension Foo {}")
506+
XCTAssertFalse(extensionDecl.hasError)
507+
508+
try withStringInterpolationParsingErrorsSuppressed {
509+
let extensionWithError = try ExtensionDeclSyntax("extension Foo {")
510+
XCTAssert(extensionWithError.hasError)
511+
}
512+
513+
XCTAssertThrowsError(try ExtensionDeclSyntax("class Foo {}"))
514+
}
503515
}

0 commit comments

Comments
 (0)