Skip to content

Commit 34a634a

Browse files
committed
Mark syntax interpolation errors in macro expansion
- This commit prepends `SyntaxStringInterpolationError` message with `Internal macro error:` when adding it as a diagnostic in `MacroExpansionContext.addDiagnostics`, so that macro expansion clarifies the error as internal to the macro, not to how it was applied. - Any unrecognized errors raised and sent to `addDiagnostics` will be added as is, without prepending them with a message. That's consistent with how it works now. - Adds a basic test for `addDiagnostics` for that behavior. - Adds end to end test with `assertMacroExpansion`, thanks @ahoppen for the hint!
1 parent a381b0d commit 34a634a

File tree

3 files changed

+90
-3
lines changed

3 files changed

+90
-3
lines changed

Sources/SwiftSyntaxBuilder/Syntax+StringInterpolation.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,15 +161,18 @@ where Self.StringInterpolation == SyntaxStringInterpolation {
161161
init(stringInterpolation: SyntaxStringInterpolation)
162162
}
163163

164-
enum SyntaxStringInterpolationError: Error, CustomStringConvertible {
164+
// Used in `MacroExpansionContext` to make a nicer error message when a
165+
// macro expansion fails with SwiftSyntaxBuilder making a syntax node
166+
// with string interpolation.
167+
@_spi(Diagnostics) public enum SyntaxStringInterpolationError: Error, CustomStringConvertible {
165168
case producedInvalidNodeType(expectedType: SyntaxProtocol.Type, actualType: SyntaxProtocol.Type)
166169
case diagnostics([Diagnostic], tree: Syntax)
167170

168171
static func producedInvalidNodeType<S: SyntaxProtocol>(expectedType: SyntaxProtocol.Type, actualNode: S) -> Self {
169172
return .producedInvalidNodeType(expectedType: expectedType, actualType: type(of: actualNode))
170173
}
171174

172-
var description: String {
175+
public var description: String {
173176
switch self {
174177
case .producedInvalidNodeType(expectedType: let expectedType, actualType: let actualType):
175178
return "Parsing the code snippet was expected to produce a \(expectedType) but produced a \(actualType)"

Sources/SwiftSyntaxMacros/MacroExpansionContext.swift

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

1313
import SwiftDiagnostics
1414
import SwiftSyntax
15+
@_spi(Diagnostics) import SwiftSyntaxBuilder
1516

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

7980
extension MacroExpansionContext {
80-
/// Add diagnostics from the error thrown during macro expansion.
81+
/// Adds diagnostics from the error thrown during a macro expansion.
8182
public func addDiagnostics(from error: Error, node: some SyntaxProtocol) {
8283
// Inspect the error to form an appropriate set of diagnostics.
8384
var diagnostics: [Diagnostic]
8485
if let diagnosticsError = error as? DiagnosticsError {
8586
diagnostics = diagnosticsError.diagnostics
8687
} else if let message = error as? DiagnosticMessage {
8788
diagnostics = [Diagnostic(node: Syntax(node), message: message)]
89+
} else if let error = error as? SyntaxStringInterpolationError {
90+
let diagnostic = Diagnostic(node: Syntax(node), message: ThrownErrorDiagnostic(message: "Internal macro error: \(error.description)"))
91+
diagnostics = [diagnostic]
8892
} else {
8993
diagnostics = [Diagnostic(node: Syntax(node), message: ThrownErrorDiagnostic(message: String(describing: error)))]
9094
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
import SwiftDiagnostics
15+
@_spi(Diagnostics) import SwiftSyntaxBuilder
16+
import SwiftSyntaxMacros
17+
import SwiftSyntaxMacroExpansion
18+
import SwiftSyntaxMacrosTestSupport
19+
import XCTest
20+
21+
22+
// An error that is not `SyntaxStringInterpolationError`, only used to verify
23+
// that other error types won't get prefixed with `Internal macro error:` when
24+
// passed to `MacroExpansionContext.addDiagnostics`.
25+
private struct DummyError: Error {
26+
static let diagnosticTestError = DummyError()
27+
}
28+
29+
// An extension macro that will fail with
30+
// `SyntaxStringInterpolationError.producedInvalidNodeType`
31+
private struct DummyMacro: ExtensionMacro {
32+
static func expansion(
33+
of node: AttributeSyntax,
34+
attachedTo declaration: some DeclGroupSyntax,
35+
providingExtensionsOf type: some TypeSyntaxProtocol,
36+
conformingTo protocols: [TypeSyntax],
37+
in context: some MacroExpansionContext
38+
) throws -> [ExtensionDeclSyntax] {
39+
let ext = try ExtensionDeclSyntax("var x: Int")
40+
return [ext]
41+
}
42+
}
43+
44+
final class MacroExpansionContextTests: XCTestCase {
45+
46+
func testMacroExpansionContextAddDiagnosticsAddsSwiftSyntaxInterpolationErrorsWithWrappingMessage(){
47+
let context = BasicMacroExpansionContext()
48+
let error = SyntaxStringInterpolationError.producedInvalidNodeType(expectedType: DeclSyntax.self, actualType: ExprSyntax.self)
49+
// Since we only care about the error switch inside of addDagnostics, we don't care about the particular node we're passing in.
50+
context.addDiagnostics(from: error, node: ExprSyntax("1"))
51+
52+
XCTAssertEqual(context.diagnostics.count, 1)
53+
XCTAssertTrue(context.diagnostics.first!.message.starts(with: "Internal macro error:"))
54+
}
55+
56+
// Verify that any other error messages do not get "Internal macro error:" prefix.
57+
func testMacroExpansionContextAddDiagnosticsUsesErrorDescriptionForDiagMessage() {
58+
let context = BasicMacroExpansionContext()
59+
let error = DummyError.diagnosticTestError
60+
61+
context.addDiagnostics(from: error, node: ExprSyntax("1"))
62+
XCTAssertEqual(context.diagnostics.count, 1)
63+
XCTAssertEqual(context.diagnostics.first!.message, String(describing: error))
64+
}
65+
66+
func testMacroExpansionSyntaxInterpolationErrorGetsPrefixed() {
67+
let expectedDiagnostic = DiagnosticSpec(
68+
message: "Internal macro error: Parsing the code snippet was expected to produce a ExtensionDeclSyntax but produced a DeclSyntax",
69+
line: 1,
70+
column: 1
71+
)
72+
73+
assertMacroExpansion(
74+
"@dummy struct Foo {}",
75+
expandedSource: "struct Foo {}",
76+
diagnostics: [expectedDiagnostic],
77+
macros: ["dummy": DummyMacro.self]
78+
)
79+
}
80+
}

0 commit comments

Comments
 (0)