From a00851102dadac895ac2f9d02863e218d8bc5440 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Tue, 8 Aug 2023 13:32:34 -0700 Subject: [PATCH] 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. --- Sources/SwiftSyntaxBuilder/CMakeLists.txt | 1 + .../DeclSyntaxParseable.swift | 57 +++++++++++++++++++ .../SyntaxNodeWithBody.swift | 35 +++++++----- ...ble+ExpressibleByStringInterpolation.swift | 19 ++++++- .../BasicFormatTests.swift | 20 ++++--- .../StringInterpolationTests.swift | 14 ++++- 6 files changed, 120 insertions(+), 26 deletions(-) create mode 100644 Sources/SwiftSyntaxBuilder/DeclSyntaxParseable.swift diff --git a/Sources/SwiftSyntaxBuilder/CMakeLists.txt b/Sources/SwiftSyntaxBuilder/CMakeLists.txt index cdb6b716f4c..895f8eee63e 100644 --- a/Sources/SwiftSyntaxBuilder/CMakeLists.txt +++ b/Sources/SwiftSyntaxBuilder/CMakeLists.txt @@ -8,6 +8,7 @@ add_swift_host_library(SwiftSyntaxBuilder ConvenienceInitializers.swift + DeclSyntaxParseable.swift Indenter.swift ResultBuilderExtensions.swift SwiftSyntaxBuilderCompatibility.swift diff --git a/Sources/SwiftSyntaxBuilder/DeclSyntaxParseable.swift b/Sources/SwiftSyntaxBuilder/DeclSyntaxParseable.swift new file mode 100644 index 00000000000..c4bb040b2fb --- /dev/null +++ b/Sources/SwiftSyntaxBuilder/DeclSyntaxParseable.swift @@ -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 { + /// 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 {} diff --git a/Sources/SwiftSyntaxBuilder/SyntaxNodeWithBody.swift b/Sources/SwiftSyntaxBuilder/SyntaxNodeWithBody.swift index bde93b3a5ac..056fa5a4e37 100644 --- a/Sources/SwiftSyntaxBuilder/SyntaxNodeWithBody.swift +++ b/Sources/SwiftSyntaxBuilder/SyntaxNodeWithBody.swift @@ -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) { @@ -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 } @@ -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) @@ -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()) } @@ -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) @@ -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) @@ -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 { @@ -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) @@ -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() } @@ -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) @@ -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) diff --git a/Sources/SwiftSyntaxBuilder/SyntaxParsable+ExpressibleByStringInterpolation.swift b/Sources/SwiftSyntaxBuilder/SyntaxParsable+ExpressibleByStringInterpolation.swift index ddc7f8025ed..cf0153ec307 100644 --- a/Sources/SwiftSyntaxBuilder/SyntaxParsable+ExpressibleByStringInterpolation.swift +++ b/Sources/SwiftSyntaxBuilder/SyntaxParsable+ExpressibleByStringInterpolation.swift @@ -20,6 +20,21 @@ 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(_ body: () throws -> T) rethrows -> T { + suppressStringInterpolationParsingErrors = true + defer { + suppressStringInterpolationParsingErrors = false + } + return try body() +} + extension SyntaxParseable { public typealias StringInterpolation = SyntaxStringInterpolation @@ -27,7 +42,9 @@ extension SyntaxParseable { /// 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( diff --git a/Tests/SwiftBasicFormatTest/BasicFormatTests.swift b/Tests/SwiftBasicFormatTest/BasicFormatTests.swift index 0dd9c935720..1cfd5bb3d4a 100644 --- a/Tests/SwiftBasicFormatTest/BasicFormatTests.swift +++ b/Tests/SwiftBasicFormatTest/BasicFormatTests.swift @@ -12,7 +12,7 @@ import SwiftBasicFormat import SwiftParser -import SwiftSyntaxBuilder +@_spi(Testing) import SwiftSyntaxBuilder import SwiftSyntax import XCTest @@ -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 + """ + ) + } } } diff --git a/Tests/SwiftSyntaxBuilderTest/StringInterpolationTests.swift b/Tests/SwiftSyntaxBuilderTest/StringInterpolationTests.swift index 7feb9791123..bb2fd7e5f53 100644 --- a/Tests/SwiftSyntaxBuilderTest/StringInterpolationTests.swift +++ b/Tests/SwiftSyntaxBuilderTest/StringInterpolationTests.swift @@ -12,7 +12,7 @@ import _SwiftSyntaxTestSupport import SwiftSyntax -import SwiftSyntaxBuilder +@_spi(Testing) import SwiftSyntaxBuilder import SwiftParser import SwiftBasicFormat @@ -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 {}")) + } }