Skip to content

Commit

Permalink
Merge pull request #2176 from natikgadzhi/macro-expansion/syntax-inte…
Browse files Browse the repository at this point in the history
…rpolation-errors

Prepend `SyntaxStringInterpolationError` errors in macro expansion with `Internal macro error:`
  • Loading branch information
ahoppen committed Sep 17, 2023
2 parents ad237ff + ec8265e commit 636a911
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 24 deletions.
2 changes: 1 addition & 1 deletion Sources/SwiftSyntaxBuilder/DeclSyntaxParseable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public extension DeclSyntaxParseable {
if let castedDecl = node.as(Self.self) {
self = castedDecl
} else {
throw SyntaxStringInterpolationError.producedInvalidNodeType(expectedType: Self.self, actualNode: node)
throw SyntaxStringInterpolationInvalidNodeTypeError(expectedType: Self.self, actualNode: node)
}
}
}
Expand Down
34 changes: 21 additions & 13 deletions Sources/SwiftSyntaxBuilder/Syntax+StringInterpolation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -161,23 +161,31 @@ where Self.StringInterpolation == SyntaxStringInterpolation {
init(stringInterpolation: SyntaxStringInterpolation)
}

enum SyntaxStringInterpolationError: Error, CustomStringConvertible {
case producedInvalidNodeType(expectedType: SyntaxProtocol.Type, actualType: SyntaxProtocol.Type)
case diagnostics([Diagnostic], tree: Syntax)
/// Describes an error when building a syntax node with string interpolation resulted in an unexpected node type.
public struct SyntaxStringInterpolationInvalidNodeTypeError: Error, CustomStringConvertible {
let expectedType: SyntaxProtocol.Type
let actualType: SyntaxProtocol.Type

/// Initialize the invalid node type error providing an expected type, and the actual node that resulted.
public init<S: SyntaxProtocol>(expectedType: SyntaxProtocol.Type, actualNode: S) {
self.expectedType = expectedType
self.actualType = type(of: actualNode)
}

static func producedInvalidNodeType<S: SyntaxProtocol>(expectedType: SyntaxProtocol.Type, actualNode: S) -> Self {
return .producedInvalidNodeType(expectedType: expectedType, actualType: type(of: actualNode))
public var description: String {
return "Parsing the code snippet was expected to produce a \(expectedType) but produced a \(actualType)"
}
}

/// A string interpolation error based on a ``SwiftDiagnostics/Diagnostic``.
struct SyntaxStringInterpolationDiagnosticError: Error, CustomStringConvertible {
let diagnostics: [Diagnostic]
let tree: Syntax

var description: String {
switch self {
case .producedInvalidNodeType(expectedType: let expectedType, actualType: let actualType):
return "Parsing the code snippet was expected to produce a \(expectedType) but produced a \(actualType)"
case .diagnostics(let diagnostics, let tree):
// Start the diagnostic on a new line so it isn't prefixed with the file, which messes up the
// column-aligned message from ``DiagnosticsFormatter``.
return "\n" + DiagnosticsFormatter.annotatedSource(tree: tree, diags: diagnostics)
}
// Start the diagnostic on a new line so it isn't prefixed with the file, which messes up the
// column-aligned message from ``DiagnosticsFormatter``.
return "\n" + DiagnosticsFormatter.annotatedSource(tree: tree, diags: diagnostics)
}
}

Expand Down
14 changes: 7 additions & 7 deletions Sources/SwiftSyntaxBuilder/SyntaxNodeWithBody.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public extension HasTrailingCodeBlock where Self: StmtSyntaxProtocol {
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)
throw SyntaxStringInterpolationInvalidNodeTypeError(expectedType: Self.self, actualNode: stmt)
}
self = castedStmt
self.body = try CodeBlockSyntax(statements: bodyBuilder())
Expand Down Expand Up @@ -121,7 +121,7 @@ public extension HasTrailingOptionalCodeBlock where Self: DeclSyntaxProtocol {
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)
throw SyntaxStringInterpolationInvalidNodeTypeError(expectedType: Self.self, actualNode: decl)
}
self = castedDecl
self.body = try CodeBlockSyntax(statements: bodyBuilder())
Expand Down Expand Up @@ -166,7 +166,7 @@ public extension HasTrailingMemberDeclBlock where Self: DeclSyntaxProtocol {
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)
throw SyntaxStringInterpolationInvalidNodeTypeError(expectedType: Self.self, actualNode: decl)
}
self = castedDecl
self.memberBlock = try MemberBlockSyntax(members: membersBuilder())
Expand Down Expand Up @@ -209,7 +209,7 @@ public extension IfExprSyntax {
) throws {
let expr = ExprSyntax("\(header) {}")
guard let ifExpr = expr.as(Self.self) else {
throw SyntaxStringInterpolationError.producedInvalidNodeType(expectedType: Self.self, actualNode: expr)
throw SyntaxStringInterpolationInvalidNodeTypeError(expectedType: Self.self, actualNode: expr)
}
self = ifExpr
self.body = try CodeBlockSyntax(statements: bodyBuilder())
Expand Down Expand Up @@ -254,7 +254,7 @@ public extension IfExprSyntax {
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)
throw SyntaxStringInterpolationInvalidNodeTypeError(expectedType: Self.self, actualNode: expr)
}
self = ifExpr
self.body = CodeBlockSyntax(statements: try bodyBuilder())
Expand Down Expand Up @@ -321,7 +321,7 @@ public extension SwitchExprSyntax {
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)
throw SyntaxStringInterpolationInvalidNodeTypeError(expectedType: Self.self, actualNode: expr)
}
self = switchExpr
self.cases = try casesBuilder()
Expand Down Expand Up @@ -355,7 +355,7 @@ public extension VariableDeclSyntax {
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)
throw SyntaxStringInterpolationInvalidNodeTypeError(expectedType: Self.self, actualNode: decl)
}
self = castedDecl
precondition(self.bindings.count == 1)
Expand Down
4 changes: 2 additions & 2 deletions Sources/SwiftSyntaxBuilder/ValidatingSyntaxNodes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ extension SyntaxProtocol {
if node.hasError {
let diagnostics = ParseDiagnosticsGenerator.diagnostics(for: node)
precondition(!diagnostics.isEmpty)
throw SyntaxStringInterpolationError.diagnostics(diagnostics, tree: Syntax(node))
throw SyntaxStringInterpolationDiagnosticError(diagnostics: diagnostics, tree: Syntax(node))
}
self = node
}
Expand All @@ -52,7 +52,7 @@ extension Trivia {
}
offset += piece.sourceLength.utf8Length
}
throw SyntaxStringInterpolationError.diagnostics(diagnostics, tree: Syntax(tree))
throw SyntaxStringInterpolationDiagnosticError(diagnostics: diagnostics, tree: Syntax(tree))
}
}
}
6 changes: 5 additions & 1 deletion Sources/SwiftSyntaxMacros/MacroExpansionContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import SwiftDiagnostics
import SwiftSyntax
import SwiftSyntaxBuilder

/// Interface to extract information about the context in which a given
/// macro is expanded.
Expand Down Expand Up @@ -77,14 +78,17 @@ private struct ThrownErrorDiagnostic: DiagnosticMessage {
}

extension MacroExpansionContext {
/// Add diagnostics from the error thrown during macro expansion.
/// Adds diagnostics from the error thrown during a macro expansion.
public func addDiagnostics(from error: Error, node: some SyntaxProtocol) {
// Inspect the error to form an appropriate set of diagnostics.
var diagnostics: [Diagnostic]
if let diagnosticsError = error as? DiagnosticsError {
diagnostics = diagnosticsError.diagnostics
} else if let message = error as? DiagnosticMessage {
diagnostics = [Diagnostic(node: Syntax(node), message: message)]
} else if let error = error as? SyntaxStringInterpolationInvalidNodeTypeError {
let diagnostic = Diagnostic(node: Syntax(node), message: ThrownErrorDiagnostic(message: "Internal macro error: \(error.description)"))
diagnostics = [diagnostic]
} else {
diagnostics = [Diagnostic(node: Syntax(node), message: ThrownErrorDiagnostic(message: String(describing: error)))]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//===----------------------------------------------------------------------===//
//
// 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 SwiftDiagnostics
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacroExpansion
import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
import XCTest

// An error that is not `SyntaxStringInterpolationError`, only used to verify
// that other error types won't get prefixed with `Internal macro error:` when
// passed to `MacroExpansionContext.addDiagnostics`.
private struct DummyError: Error {
static let diagnosticTestError = DummyError()
}

// An extension macro that will fail with
// `SyntaxStringInterpolationError.producedInvalidNodeType`
private struct DummyMacro: ExtensionMacro {
static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {
let ext = try ExtensionDeclSyntax("var x: Int")
return [ext]
}
}

final class StringInterpolationErrorTests: XCTestCase {

func testMacroExpansionContextAddDiagnosticsAddsSwiftSyntaxInterpolationErrorsWithWrappingMessage() throws {
let context = BasicMacroExpansionContext()
let error = SyntaxStringInterpolationInvalidNodeTypeError(expectedType: DeclSyntax.self, actualNode: ExprSyntax("test"))

// Since we only care about the error switch inside of addDagnostics, we don't care about the particular node we're passing in
context.addDiagnostics(from: error, node: ExprSyntax("1"))
XCTAssertEqual(context.diagnostics.count, 1)
let diagnostic = try XCTUnwrap(context.diagnostics.first)
XCTAssertTrue(diagnostic.message.starts(with: "Internal macro error:"))
}

// Verify that any other error messages do not get "Internal macro error:" prefix.
func testMacroExpansionContextAddDiagnosticsUsesErrorDescriptionForDiagMessage() throws {
let context = BasicMacroExpansionContext()
let error = DummyError.diagnosticTestError

context.addDiagnostics(from: error, node: ExprSyntax("1"))
XCTAssertEqual(context.diagnostics.count, 1)
let diagnostic = try XCTUnwrap(context.diagnostics.first)
XCTAssertEqual(diagnostic.message, String(describing: error))
}

func testMacroExpansionSyntaxInterpolationErrorGetsPrefixed() {
let expectedDiagnostic = DiagnosticSpec(
message: "Internal macro error: Parsing the code snippet was expected to produce a ExtensionDeclSyntax but produced a DeclSyntax",
line: 1,
column: 1
)

assertMacroExpansion(
"@dummy struct Foo {}",
expandedSource: "struct Foo {}",
diagnostics: [expectedDiagnostic],
macros: ["dummy": DummyMacro.self]
)
}
}

0 comments on commit 636a911

Please sign in to comment.