Skip to content

Support construction of DeclSyntax subtypes from string interpolation #2032

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Sources/SwiftSyntaxBuilder/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

add_swift_host_library(SwiftSyntaxBuilder
ConvenienceInitializers.swift
DeclSyntaxParseable.swift
Indenter.swift
ResultBuilderExtensions.swift
SwiftSyntaxBuilderCompatibility.swift
Expand Down
57 changes: 57 additions & 0 deletions Sources/SwiftSyntaxBuilder/DeclSyntaxParseable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import SwiftSyntax

/// Adds an initializer that allows the creation of declaration from string
/// interpolations.
///
/// - Warning: This protocol is considered an implementation detail. Do not rely
/// on it, only the initializer that it adds.
public protocol DeclSyntaxParseable: DeclSyntaxProtocol {}
public extension DeclSyntaxParseable {
Comment on lines +20 to +21
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason init(_ stringInterpolation: SyntaxNodeString) can't be just in extension DeclSyntaxProtocol?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because, for example, AccessorDeclSyntax does not go through parseDecl, it has its own parser entry function. And thus, trying to call parser.parseDecl and casting it to AccessorDeclSyntax wouldn’t work.

/// Create a syntax node from the given string interpolation.
///
/// This initializer throws if the syntax node was not able to be parsed as
/// this type, e.g. when calling `ClassDeclSyntax("actor Foo {})`.
///
/// If there are syntax errors in the string, the initializer will return a
/// node that contains errors without throwing.
init(_ stringInterpolation: SyntaxNodeString) throws {
let node: DeclSyntax = "\(stringInterpolation)"
if let castedDecl = node.as(Self.self) {
self = castedDecl
} else {
throw SyntaxStringInterpolationError.producedInvalidNodeType(expectedType: Self.self, actualNode: node)
}
}
}

// These are all the declarations that get parsed from `parseDecl`
extension ActorDeclSyntax: DeclSyntaxParseable {}
extension AssociatedTypeDeclSyntax: DeclSyntaxParseable {}
extension EnumCaseDeclSyntax: DeclSyntaxParseable {}
extension ClassDeclSyntax: DeclSyntaxParseable {}
extension DeinitializerDeclSyntax: DeclSyntaxParseable {}
extension EnumDeclSyntax: DeclSyntaxParseable {}
extension ExtensionDeclSyntax: DeclSyntaxParseable {}
extension FunctionDeclSyntax: DeclSyntaxParseable {}
extension ImportDeclSyntax: DeclSyntaxParseable {}
extension VariableDeclSyntax: DeclSyntaxParseable {}
extension MacroDeclSyntax: DeclSyntaxParseable {}
extension OperatorDeclSyntax: DeclSyntaxParseable {}
extension MacroExpansionDeclSyntax: DeclSyntaxParseable {}
extension PrecedenceGroupDeclSyntax: DeclSyntaxParseable {}
extension ProtocolDeclSyntax: DeclSyntaxParseable {}
extension StructDeclSyntax: DeclSyntaxParseable {}
extension SubscriptDeclSyntax: DeclSyntaxParseable {}
extension TypeAliasDeclSyntax: DeclSyntaxParseable {}
35 changes: 20 additions & 15 deletions Sources/SwiftSyntaxBuilder/SyntaxNodeWithBody.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,18 @@ import SwiftSyntax

// MARK: - PartialSyntaxNode

@available(*, deprecated, renamed: "SyntaxNodeString")
public typealias PartialSyntaxNodeString = SyntaxNodeString

/// A type that is expressible by string interpolation the same way that syntax
/// nodes are, but instead of producing a node, it stores the string interpolation
/// text. Used to represent partial syntax nodes in initializers that take a
/// text.
///
/// Used to represent partial syntax nodes in initializers that take a
/// trailing code block.
///
/// This type should always be constructed using string interpolation.
public struct PartialSyntaxNodeString: SyntaxExpressibleByStringInterpolation {
public struct SyntaxNodeString: SyntaxExpressibleByStringInterpolation {
let sourceText: [UInt8]

public init(stringInterpolation: SyntaxStringInterpolation) {
Expand All @@ -29,7 +34,7 @@ public struct PartialSyntaxNodeString: SyntaxExpressibleByStringInterpolation {
}

extension SyntaxStringInterpolation {
public mutating func appendInterpolation(_ value: PartialSyntaxNodeString) {
public mutating func appendInterpolation(_ value: SyntaxNodeString) {
sourceText.append(contentsOf: value.sourceText)
self.lastIndentation = nil
}
Expand Down Expand Up @@ -59,11 +64,11 @@ public protocol HasTrailingCodeBlock {
/// ```
///
/// 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") {}`
init(_ header: PartialSyntaxNodeString, @CodeBlockItemListBuilder bodyBuilder: () throws -> CodeBlockItemListSyntax) rethrows
init(_ header: SyntaxNodeString, @CodeBlockItemListBuilder bodyBuilder: () throws -> CodeBlockItemListSyntax) rethrows
}

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

extension CatchClauseSyntax: HasTrailingCodeBlock {
public init(_ header: PartialSyntaxNodeString, @CodeBlockItemListBuilder bodyBuilder: () throws -> CodeBlockItemListSyntax) rethrows {
public init(_ header: SyntaxNodeString, @CodeBlockItemListBuilder bodyBuilder: () throws -> CodeBlockItemListSyntax) rethrows {
self = CatchClauseSyntax("\(header) {}")
self.body = try CodeBlockSyntax(statements: bodyBuilder())
}
Expand Down Expand Up @@ -109,11 +114,11 @@ public protocol HasTrailingOptionalCodeBlock {
/// ```
///
/// 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") {}`
init(_ header: PartialSyntaxNodeString, @CodeBlockItemListBuilder bodyBuilder: () throws -> CodeBlockItemListSyntax) throws
init(_ header: SyntaxNodeString, @CodeBlockItemListBuilder bodyBuilder: () throws -> CodeBlockItemListSyntax) throws
}

public extension HasTrailingOptionalCodeBlock where Self: DeclSyntaxProtocol {
init(_ header: PartialSyntaxNodeString, @CodeBlockItemListBuilder bodyBuilder: () throws -> CodeBlockItemListSyntax) throws {
init(_ header: SyntaxNodeString, @CodeBlockItemListBuilder bodyBuilder: () throws -> CodeBlockItemListSyntax) throws {
let decl = DeclSyntax("\(header) {}")
guard let castedDecl = decl.as(Self.self) else {
throw SyntaxStringInterpolationError.producedInvalidNodeType(expectedType: Self.self, actualNode: decl)
Expand Down Expand Up @@ -154,11 +159,11 @@ public protocol HasTrailingMemberDeclBlock {
/// ```
///
/// 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") {}`
init(_ header: PartialSyntaxNodeString, @MemberBlockItemListBuilder membersBuilder: () throws -> MemberBlockItemListSyntax) throws
init(_ header: SyntaxNodeString, @MemberBlockItemListBuilder membersBuilder: () throws -> MemberBlockItemListSyntax) throws
}

public extension HasTrailingMemberDeclBlock where Self: DeclSyntaxProtocol {
init(_ header: PartialSyntaxNodeString, @MemberBlockItemListBuilder membersBuilder: () throws -> MemberBlockItemListSyntax) throws {
init(_ header: SyntaxNodeString, @MemberBlockItemListBuilder membersBuilder: () throws -> MemberBlockItemListSyntax) throws {
let decl = DeclSyntax("\(header) {}")
guard let castedDecl = decl.as(Self.self) else {
throw SyntaxStringInterpolationError.producedInvalidNodeType(expectedType: Self.self, actualNode: decl)
Expand Down Expand Up @@ -198,7 +203,7 @@ public extension IfExprSyntax {
///
/// Throws an error if `header` does not start an `if` expression. E.g. if calling `try IfExprSyntax("while true") {}`
init(
_ header: PartialSyntaxNodeString,
_ header: SyntaxNodeString,
@CodeBlockItemListBuilder bodyBuilder: () throws -> CodeBlockItemListSyntax,
@CodeBlockItemListBuilder `else` elseBuilder: () throws -> CodeBlockItemListSyntax? = { nil }
) throws {
Expand Down Expand Up @@ -246,7 +251,7 @@ public extension IfExprSyntax {
/// ```
///
/// Throws an error if `header` does not start an `if` expression. E.g. if calling `try IfExprSyntax("while true", bodyBuilder: {}, elseIf: {})`
init(_ header: PartialSyntaxNodeString, @CodeBlockItemListBuilder bodyBuilder: () throws -> CodeBlockItemListSyntax, elseIf: IfExprSyntax) throws {
init(_ header: SyntaxNodeString, @CodeBlockItemListBuilder bodyBuilder: () throws -> CodeBlockItemListSyntax, elseIf: IfExprSyntax) throws {
let expr = ExprSyntax("\(header) {}")
guard let ifExpr = expr.as(Self.self) else {
throw SyntaxStringInterpolationError.producedInvalidNodeType(expectedType: Self.self, actualNode: expr)
Expand Down Expand Up @@ -279,7 +284,7 @@ extension SwitchCaseSyntax {
/// ```
///
/// Throws an error if `header` does not start a switch case item. E.g. if calling `try SwitchCaseSyntax("func foo") {}`
public init(_ header: PartialSyntaxNodeString, @CodeBlockItemListBuilder statementsBuilder: () throws -> CodeBlockItemListSyntax) rethrows {
public init(_ header: SyntaxNodeString, @CodeBlockItemListBuilder statementsBuilder: () throws -> CodeBlockItemListSyntax) rethrows {
self = SwitchCaseSyntax("\(header)")
self.statements = try statementsBuilder()
}
Expand Down Expand Up @@ -313,7 +318,7 @@ public extension SwitchExprSyntax {
/// ```
///
/// Throws an error if `header` does not start a switch expression. E.g. if calling `try SwitchExprSyntax("if x < 42") {}`
init(_ header: PartialSyntaxNodeString, @SwitchCaseListBuilder casesBuilder: () throws -> SwitchCaseListSyntax = { SwitchCaseListSyntax([]) }) throws {
init(_ header: SyntaxNodeString, @SwitchCaseListBuilder casesBuilder: () throws -> SwitchCaseListSyntax = { SwitchCaseListSyntax([]) }) throws {
let expr = ExprSyntax("\(header) {}")
guard let switchExpr = expr.as(Self.self) else {
throw SyntaxStringInterpolationError.producedInvalidNodeType(expectedType: Self.self, actualNode: expr)
Expand Down Expand Up @@ -347,7 +352,7 @@ public extension VariableDeclSyntax {
/// ```
///
/// Throws an error if `header` does not start a variable declaration. E.g. if calling `try VariableDeclSyntax("func foo") {}`
init(_ header: PartialSyntaxNodeString, @CodeBlockItemListBuilder accessor: () throws -> CodeBlockItemListSyntax) throws {
init(_ header: SyntaxNodeString, @CodeBlockItemListBuilder accessor: () throws -> CodeBlockItemListSyntax) throws {
let decl = DeclSyntax("\(header) {}")
guard let castedDecl = decl.as(Self.self) else {
throw SyntaxStringInterpolationError.producedInvalidNodeType(expectedType: Self.self, actualNode: decl)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,31 @@ import SwiftParserDiagnostics
import OSLog
#endif

fileprivate var suppressStringInterpolationParsingErrors = false

/// Run the body, disabling any runtime warnings about syntax error in string
/// interpolation inside the body.
///
/// Used to test the behavior of string interpolation with syntax errors.
@_spi(Testing)
public func withStringInterpolationParsingErrorsSuppressed<T>(_ body: () throws -> T) rethrows -> T {
suppressStringInterpolationParsingErrors = true
defer {
suppressStringInterpolationParsingErrors = false
}
return try body()
}

extension SyntaxParseable {
public typealias StringInterpolation = SyntaxStringInterpolation

/// Assuming that this node contains a syntax error, log it using OSLog if we
/// are on a platform that supports OSLog, otherwise don't do anything.
private func logStringInterpolationParsingError() {
#if canImport(OSLog) && !SWIFTSYNTAX_NO_OSLOG_DEPENDENCY
if #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, macCatalyst 14.0, *) {
if #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, macCatalyst 14.0, *),
!suppressStringInterpolationParsingErrors
{
let diagnostics = ParseDiagnosticsGenerator.diagnostics(for: self)
let formattedDiagnostics = DiagnosticsFormatter().annotatedSource(tree: self, diags: diagnostics)
Logger(subsystem: "SwiftSyntax", category: "ParseError").fault(
Expand Down
20 changes: 11 additions & 9 deletions Tests/SwiftBasicFormatTest/BasicFormatTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import SwiftBasicFormat
import SwiftParser
import SwiftSyntaxBuilder
@_spi(Testing) import SwiftSyntaxBuilder
import SwiftSyntax

import XCTest
Expand Down Expand Up @@ -491,15 +491,17 @@ final class BasicFormatTest: XCTestCase {
}

func testUnexpectedIsNotFormatted() {
let expr: ExprSyntax = """
let foo=1
"""

assertFormatted(
tree: expr,
expected: """
withStringInterpolationParsingErrorsSuppressed {
let expr: ExprSyntax = """
let foo=1
"""
)

assertFormatted(
tree: expr,
expected: """
let foo=1
"""
)
}
}
}
14 changes: 13 additions & 1 deletion Tests/SwiftSyntaxBuilderTest/StringInterpolationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import _SwiftSyntaxTestSupport
import SwiftSyntax
import SwiftSyntaxBuilder
@_spi(Testing) import SwiftSyntaxBuilder
import SwiftParser
import SwiftBasicFormat

Expand Down Expand Up @@ -500,4 +500,16 @@ final class StringInterpolationTests: XCTestCase {
)
}
}

func testExtensionDeclFromStringInterpolation() throws {
let extensionDecl = try ExtensionDeclSyntax("extension Foo {}")
XCTAssertFalse(extensionDecl.hasError)

try withStringInterpolationParsingErrorsSuppressed {
let extensionWithError = try ExtensionDeclSyntax("extension Foo {")
XCTAssert(extensionWithError.hasError)
}

XCTAssertThrowsError(try ExtensionDeclSyntax("class Foo {}"))
}
}