diff --git a/Package.swift b/Package.swift index f975f2613..2a10324b3 100644 --- a/Package.swift +++ b/Package.swift @@ -92,7 +92,12 @@ let package = Package( ["-Xlinker", "-undefined", "-Xlinker", "dynamic_lookup"])]), // Idea: -mark_dead_strippable_dylib - + .testTarget(name: "SwiftGodotMacroTests", + dependencies: [ + "SwiftGodotMacroLibrary", + .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax") + ]) + // Test suite for SwiftGodot // .testTarget( // name: "SwiftGodotTests", diff --git a/Sources/SwiftGodotMacroLibrary/InitSwiftExtensionMacro.swift b/Sources/SwiftGodotMacroLibrary/InitSwiftExtensionMacro.swift new file mode 100644 index 000000000..0eecd5b40 --- /dev/null +++ b/Sources/SwiftGodotMacroLibrary/InitSwiftExtensionMacro.swift @@ -0,0 +1,51 @@ +// +// InitSwiftExtensionMacro.swift +// SwiftGodot +// +// Created by Marquis Kurt on 5/27/23. +// + +import Foundation +import SwiftCompilerPlugin +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +public struct InitSwiftExtensionMacro: DeclarationMacro { + public static func expansion(of node: some FreestandingMacroExpansionSyntax, + in context: some MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax] { + + guard let cDecl = node.argumentList.first?.expression else { + fatalError("compiler bug: the macro does not have any arguments") + } + guard let types = node.argumentList.last?.expression else { + fatalError("compiler bug: the macro does not have any arguments") + } + + let initModule: DeclSyntax = """ + @_cdecl(\(raw: cDecl.description)) public func enterExtension(interface: OpaquePointer?, library: OpaquePointer?, extension: OpaquePointer?) -> UInt8 { + guard let library, let interface, let `extension` else { + print("Error: Not all parameters were initialized.") + return 0 + } + let deinitHook: (GDExtension.InitializationLevel) -> Void = { _ in } + initializeSwiftModule(interface, library, `extension`, initHook: setupExtension, deInitHook: deinitHook) + return 1 + } + """ + + let setupModule: DeclSyntax = """ + func setupExtension(level: GDExtension.InitializationLevel) { + let types = \(types) + switch level { + case .scene: + types.forEach(register) + default: + break + } + } + """ + return [initModule, setupModule] + } +} diff --git a/Sources/SwiftGodotMacroLibrary/MacroGodot.swift b/Sources/SwiftGodotMacroLibrary/MacroGodot.swift index 4eccc3bd5..223ef2b5a 100644 --- a/Sources/SwiftGodotMacroLibrary/MacroGodot.swift +++ b/Sources/SwiftGodotMacroLibrary/MacroGodot.swift @@ -228,6 +228,11 @@ struct godotMacrosPlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ GodotMacro.self, GodotCallable.self, - GodotExport.self + GodotExport.self, + InitSwiftExtensionMacro.self, + NativeHandleDiscardingMacro.self, + PickerNameProviderMacro.self, + SceneTreeMacro.self, + Texture2DLiteralMacro.self ] } diff --git a/Sources/SwiftGodotMacroLibrary/NativeHandleDiscardingMacro.swift b/Sources/SwiftGodotMacroLibrary/NativeHandleDiscardingMacro.swift new file mode 100644 index 000000000..8e99da2da --- /dev/null +++ b/Sources/SwiftGodotMacroLibrary/NativeHandleDiscardingMacro.swift @@ -0,0 +1,55 @@ +// +// NativeHandleDiscardingMacro.swift +// SwiftGodot +// +// Created by Marquis Kurt on 5/27/23. +// + +import Foundation +import SwiftCompilerPlugin +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +public struct NativeHandleDiscardingMacro: MemberMacro { + enum ProviderDiagnostic: String, DiagnosticMessage { + case notAClass + case missingNode + var severity: DiagnosticSeverity { + switch self { + case .notAClass: return .error + case .missingNode: return .error + } + } + + var message: String { + switch self { + case .notAClass: + return "@NativeHandleDiscarding can only be applied to a 'class'" + case .missingNode: + return "@NativeHandleDiscarding requires inheritance to 'Node' or a subclass" + } + } + + var diagnosticID: MessageID { + MessageID(domain: "SwiftGodotMacros", id: rawValue) + } + } + + public static func expansion(of node: AttributeSyntax, + providingMembersOf declaration: some DeclGroupSyntax, + in context: some MacroExpansionContext) throws -> [DeclSyntax] { + guard declaration.as(ClassDeclSyntax.self) != nil else { + let classError = Diagnostic(node: declaration.root, message: ProviderDiagnostic.notAClass) + context.diagnose(classError) + return [] + } + + let initSyntax = try InitializerDeclSyntax("required init(nativeHandle _: UnsafeRawPointer)") { + StmtSyntax("fatalError(\"init(nativeHandle:) has not been implemented\")") + } + + return [DeclSyntax(initSyntax)] + } +} diff --git a/Sources/SwiftGodotMacroLibrary/PickerNameProviderMacro.swift b/Sources/SwiftGodotMacroLibrary/PickerNameProviderMacro.swift new file mode 100644 index 000000000..e95f7be63 --- /dev/null +++ b/Sources/SwiftGodotMacroLibrary/PickerNameProviderMacro.swift @@ -0,0 +1,104 @@ +// +// PickerNameProviderMacro.swift +// SwiftGodot +// +// Created by Marquis Kurt on 6/9/23. +// + +import Foundation +import SwiftCompilerPlugin +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +public struct PickerNameProviderMacro: ExtensionMacro { + enum ProviderDiagnostic: String, DiagnosticMessage { + case notAnEnum + case missingInt + var severity: DiagnosticSeverity { + switch self { + case .notAnEnum: return .error + case .missingInt: return .error + } + } + + var message: String { + switch self { + case .notAnEnum: + return "@PickerNameProvider can only be applied to an 'enum'" + case .missingInt: + return "@PickerNameProvider requires an Int backing" + } + } + + var diagnosticID: MessageID { + MessageID(domain: "SwiftGodotMacros", id: rawValue) + } + } + + public static func expansion(of node: AttributeSyntax, + attachedTo declaration: some DeclGroupSyntax, + providingExtensionsOf type: some TypeSyntaxProtocol, + conformingTo protocols: [TypeSyntax], + in context: some MacroExpansionContext) throws -> [ExtensionDeclSyntax] { + + guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { + let enumError = Diagnostic(node: declaration.root, message: ProviderDiagnostic.notAnEnum) + context.diagnose(enumError) + return [] + } + + guard let inheritors = enumDecl.inheritanceClause?.inheritedTypes else { + let missingInt = Diagnostic(node: declaration.root, message: ProviderDiagnostic.missingInt) + context.diagnose(missingInt) + return [] + } + + let types = inheritors.map { $0.type.as(IdentifierTypeSyntax.self) } + let names = types.map { $0?.name.text } + + guard names.contains("Int") else { + let missingInt = Diagnostic(node: declaration.root, message: ProviderDiagnostic.missingInt) + context.diagnose(missingInt) + return [] + } + + let members = enumDecl.memberBlock.members + let cases = members.compactMap { $0.decl.as(EnumCaseDeclSyntax.self) } + let elements = cases.flatMap { $0.elements } + + let nameDeclBase = try VariableDeclSyntax("var name: String") { + try SwitchExprSyntax("switch self") { + for element in elements { + SwitchCaseSyntax( + """ + case .\(element.name): + return \(literal: element.name.text.capitalized) + """ + ) + } + } + } + + var nameDecl = nameDeclBase + for modifier in enumDecl.modifiers { + nameDecl.modifiers.append(modifier) + } + + let caseIterableExtensionDecl: DeclSyntax = + """ + extension \(type.trimmed): CaseIterable {} + """ + + guard let caseIterableExtension = caseIterableExtensionDecl.as(ExtensionDeclSyntax.self) else { + return [] + } + + let nameableExtension = try ExtensionDeclSyntax("extension \(type.trimmed): Nameable") { + DeclSyntax(nameDecl) + } + + return [caseIterableExtension, nameableExtension] + } +} diff --git a/Sources/SwiftGodotMacroLibrary/SceneTreeMacro.swift b/Sources/SwiftGodotMacroLibrary/SceneTreeMacro.swift new file mode 100644 index 000000000..448d383e8 --- /dev/null +++ b/Sources/SwiftGodotMacroLibrary/SceneTreeMacro.swift @@ -0,0 +1,97 @@ +// +// SceneTreeMacro.swift +// SwiftGodot +// +// Created by Marquis Kurt on 6/22/23. +// + +import Foundation +import SwiftCompilerPlugin +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +public struct SceneTreeMacro: AccessorMacro { + enum ProviderDiagnostic: String, DiagnosticMessage { + case missingPathArgument + case invalidDeclaration + case missingTypeAnnotation + case nonOptionalTypeAnnotation + + var severity: DiagnosticSeverity { .error } + + var message: String { + switch self { + case .missingPathArgument: + "Missing argument 'path'" + case .invalidDeclaration: + "SceneTree can only be applied to stored properties" + case .missingTypeAnnotation: + "SceneTree requires an explicit type declaration" + case .nonOptionalTypeAnnotation: + "Stored properties with SceneTree must be marked as Optional" + } + } + + var diagnosticID: MessageID { + MessageID(domain: "SwiftGodotMacros", id: rawValue) + } + } + + struct MarkOptionalMessage: FixItMessage { + var message: String { + "Mark as Optional" + } + + var fixItID: SwiftDiagnostics.MessageID { + ProviderDiagnostic.nonOptionalTypeAnnotation.diagnosticID + } + + } + + public static func expansion(of node: AttributeSyntax, + providingAccessorsOf declaration: some DeclSyntaxProtocol, + in context: some MacroExpansionContext) throws -> [AccessorDeclSyntax] { + guard let argument = node.argument?.as(TupleExprElementListSyntax.self)?.first?.expression else { + let missingArgErr = Diagnostic(node: node.root, message: ProviderDiagnostic.missingPathArgument) + context.diagnose(missingArgErr) + return [ + """ + get { getNodeOrNull(path: NodePath(stringLiteral: "")) as? Node } + """ + ] + } + guard let varDecl = declaration.as(VariableDeclSyntax.self) else { + let invalidUsageErr = Diagnostic(node: node.root, message: ProviderDiagnostic.invalidDeclaration) + context.diagnose(invalidUsageErr) + return [] + } + guard let nodeType = varDecl.bindings.first?.typeAnnotation?.type else { + let missingAnnotationErr = Diagnostic(node: node.root, message: ProviderDiagnostic.missingTypeAnnotation) + context.diagnose(missingAnnotationErr) + return [] + } + + guard let optional = nodeType.as(OptionalTypeSyntax.self) else { + let newOptional = OptionalTypeSyntax(wrappedType: nodeType) + let addOptionalFix = FixIt(message: MarkOptionalMessage(), + changes: [.replace(oldNode: Syntax(nodeType), newNode: Syntax(newOptional))]) + let nonOptional = Diagnostic(node: nodeType.root, + message: ProviderDiagnostic.nonOptionalTypeAnnotation, + fixIts: [addOptionalFix]) + context.diagnose(nonOptional) + return [ + """ + get { getNodeOrNull(path: NodePath(stringLiteral: \(argument))) as? \(nodeType) } + """ + ] + } + + return [ + """ + get { getNodeOrNull(path: NodePath(stringLiteral: \(argument))) as? \(optional.wrappedType) } + """ + ] + } +} diff --git a/Sources/SwiftGodotMacroLibrary/TextureLiteralMacro.swift b/Sources/SwiftGodotMacroLibrary/TextureLiteralMacro.swift new file mode 100644 index 000000000..c8c4b60fd --- /dev/null +++ b/Sources/SwiftGodotMacroLibrary/TextureLiteralMacro.swift @@ -0,0 +1,56 @@ +// +// TextureLiteralMacro.swift +// SwiftGodot +// +// Created by Marquis Kurt on 6/11/23. +// + +import Foundation +import SwiftCompilerPlugin +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +public struct Texture2DLiteralMacro: ExpressionMacro { + enum ProviderDiagnostic: String, DiagnosticMessage { + case missingArguments + var severity: DiagnosticSeverity { + switch self { + case .missingArguments: return .error + } + } + + var message: String { + switch self { + case .missingArguments: + return "Argument 'path' is missing." + } + } + + var diagnosticID: MessageID { + MessageID(domain: "SwiftGodotMacros", id: rawValue) + } + } + + public static func expansion(of node: some FreestandingMacroExpansionSyntax, + in context: some MacroExpansionContext) throws -> ExprSyntax { + guard let argument = node.argumentList.first?.expression else { + let argumentError = Diagnostic(node: node.root, message: ProviderDiagnostic.missingArguments) + context.diagnose(argumentError) + return "\"\"" + } + let location: AbstractSourceLocation = context.location(of: node)! + return """ + { + guard let texture: Texture2D = GD.load(path: \(argument)) else { + preconditionFailure( + "Texture could not be loaded.", + file: \(raw: location.file), + line: \(raw: location.line)) + } + return texture + }() + """ + } +} diff --git a/Sources/SwiftGodotMacros/MacroDefs.swift b/Sources/SwiftGodotMacros/MacroDefs.swift index fadbe718a..15c46f15a 100644 --- a/Sources/SwiftGodotMacros/MacroDefs.swift +++ b/Sources/SwiftGodotMacros/MacroDefs.swift @@ -8,7 +8,92 @@ import Foundation import SwiftGodot -@attached(member, +// MARK: - Freestanding Macros + +/// A macro used to write an entrypoint for a Godot extension. +/// +/// For example, to initialize a Swift extension to Godot with custom types: +/// ```swift +/// class MySprite: Sprite2D { ... } +/// class MyControl: Control { ... } +/// +/// #initSwiftExtension(cdecl: "myextension_entry_point", +/// types: [MySprite.self, MyControl.self]) +/// ``` +/// +/// - Parameter cdecl: The name of the entrypoint exposed to C. +/// - Parameter types: The node types that should be registered with Godot. +@freestanding(declaration, names: named(enterExtension), named(setupExtension)) +public macro initSwiftExtension(cdecl: String, + types: [T.Type]) = #externalMacro(module: "SwiftGodotMacroLibrary", + type: "InitSwiftExtensionMacro") + +/// A macro that instantiates a `Texture2D` from a specified resource path. If the texture cannot be created, a +/// `preconditionFailure` will be thrown. +/// +/// Use this to quickly instantiate a `Texture2D`: +/// ```swift +/// func makeSprite() -> Sprite2D { +/// let sprite = Sprite2D() +/// sprite.texture = #texture2DLiteratl("res://assets/playersprite.png") +/// } +/// ``` +@freestanding(expression) +public macro texture2DLiteral(_ path: String) -> Texture2D = #externalMacro(module: "SwiftGodotMacroLibrary", + type: "Texture2DLiteralMacro") + +// MARK: - Attached Macros + +/// A macro that enables an enumeration to be visible to the Godot editor. +/// +/// Use this macro with `ClassInfo.registerEnum` to register this enumeration's visibility in the Godot editor. +/// +/// ```swift +/// @PickerNameProvider +/// enum PlayerClass: Int { +/// case barbarian +/// case mage +/// case wizard +/// } +/// ``` +/// +/// - Important: The enumeration should have an `Int` backing to allow being represented as an integer value by Godot. +@attached(extension, conformances: CaseIterable, Nameable, names: named(name)) +//@attached(member, names: named(name)) +public macro PickerNameProvider() = #externalMacro(module: "SwiftGodotMacroLibrary", type: "PickerNameProviderMacro") + + +/// A macro that automatically implements `init(nativeHandle:)` for nodes. +/// +/// Use this for a class that has a required initializer with an `UnsafeRawPointer`. +/// +/// ```swift +/// @NativeHandleDiscarding +/// class MySprite: Sprite2D { +/// ... +/// } +/// ``` +@attached(member, names: named(init(nativeHandle:))) +public macro NativeHandleDiscarding() = #externalMacro(module: "SwiftGodotMacroLibrary", + type: "NativeHandleDiscardingMacro") + +/// A macro that finds and assigns a node from the scene tree to a stored property. +/// +/// Use this to quickly assign a stored property to a node in the scene tree. +/// ```swift +/// class MyNode: Node2D { +/// @SceneTree(path: "Entities/Player") +/// var player: CharacterBody2D? +/// } +/// ``` +/// +/// - Important: This property will become a computed property, and it cannot be reassigned later. +@attached(accessor) +public macro SceneTree(path: String) = #externalMacro(module: "SwiftGodotMacroLibrary", type: "SceneTreeMacro") + +// TODO: Add doc comments for these macros + +@attached(member, names: named (init(nativeHandle:)), named (init()), named(_initClass), arbitrary) public macro Godot() = #externalMacro(module: "SwiftGodotMacroLibrary", type: "GodotMacro") @@ -17,4 +102,3 @@ public macro Callable() = #externalMacro(module: "SwiftGodotMacroLibrary", type: @attached(peer, names: prefixed(_mproxy_get_), prefixed(_mproxy_set_), arbitrary) public macro Export(_ hint: PropertyHint = .none, _ hintStr: String? = nil) = #externalMacro(module: "SwiftGodotMacroLibrary", type: "GodotExport") - diff --git a/Tests/SwiftGodotMacrosTests/InitSwiftExtensionMacroTests.swift b/Tests/SwiftGodotMacrosTests/InitSwiftExtensionMacroTests.swift new file mode 100644 index 000000000..82eece3c4 --- /dev/null +++ b/Tests/SwiftGodotMacrosTests/InitSwiftExtensionMacroTests.swift @@ -0,0 +1,47 @@ +// +// SwiftGodotInitSwiftExtensionMacroTests.swift +// SwiftGodot +// +// Created by Marquis Kurt on 6/9/23. +// + +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest +import SwiftGodotMacroLibrary + +final class InitSwiftExtensionMacroTests: XCTestCase { + let testMacros: [String: Macro.Type] = [ + "initSwiftExtension": InitSwiftExtensionMacro.self + ] + + func testInitSwiftExtensionMacro() { + assertMacroExpansion( + """ + #initSwiftExtension(cdecl: "libchrysalis_entry_point", types: [ChrysalisNode.self]) + """, + expandedSource: """ + @_cdecl("libchrysalis_entry_point") public func enterExtension(interface: OpaquePointer?, library: OpaquePointer?, extension: OpaquePointer?) -> UInt8 { + guard let library, let interface, let `extension` else { + print("Error: Not all parameters were initialized.") + return 0 + } + let deinitHook: (GDExtension.InitializationLevel) -> Void = { _ in + } + initializeSwiftModule(interface, library, `extension`, initHook: setupExtension, deInitHook: deinitHook) + return 1 + } + func setupExtension(level: GDExtension.InitializationLevel) { + let types = [ChrysalisNode.self] + switch level { + case .scene: + types.forEach(register) + default: + break + } + } + """, + macros: testMacros + ) + } +} diff --git a/Tests/SwiftGodotMacrosTests/NativeHandleDiscardingMacroTests.swift b/Tests/SwiftGodotMacrosTests/NativeHandleDiscardingMacroTests.swift new file mode 100644 index 000000000..56e231d36 --- /dev/null +++ b/Tests/SwiftGodotMacrosTests/NativeHandleDiscardingMacroTests.swift @@ -0,0 +1,59 @@ +// +// SwiftGodotNativeHandleDiscardingMacroTests.swift +// SwiftGodot +// +// Created by Marquis Kurt on 6/9/23. +// + +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest +import SwiftGodotMacroLibrary + +final class NativeHandleDiscardingMacroTests: XCTestCase { + let testMacros: [String: Macro.Type] = [ + "NativeHandleDiscarding": NativeHandleDiscardingMacro.self + ] + + func testNativeHandleDiscardingMacro() { + assertMacroExpansion( + """ + @NativeHandleDiscarding + class MyNode: Sprite2D { + var collider: CollisionShape2D? + } + """, + expandedSource: """ + + class MyNode: Sprite2D { + var collider: CollisionShape2D? + + required init(nativeHandle _: UnsafeRawPointer) {fatalError("init(nativeHandle:) has not been implemented") + } + } + """, + macros: testMacros + ) + } + + func testNativeHandleDiscardingMacroDiagnostics() { + assertMacroExpansion( + """ + @NativeHandleDiscarding + struct MyNode: Sprite2D { + var collider: CollisionShape2D? + } + """, + expandedSource: """ + + struct MyNode: Sprite2D { + var collider: CollisionShape2D? + } + """, + diagnostics: [ + DiagnosticSpec(message: "@NativeHandleDiscarding can only be applied to a 'class'", line: 1, column: 1) + ], + macros: testMacros + ) + } +} diff --git a/Tests/SwiftGodotMacrosTests/PickerNameProviderMacroTests.swift b/Tests/SwiftGodotMacrosTests/PickerNameProviderMacroTests.swift new file mode 100644 index 000000000..ddfde702c --- /dev/null +++ b/Tests/SwiftGodotMacrosTests/PickerNameProviderMacroTests.swift @@ -0,0 +1,91 @@ +// +// SwiftGodotNamePickerMacroTests.swift +// SwiftGodot +// +// Created by Marquis Kurt on 6/9/23. +// + +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest +import SwiftGodotMacroLibrary + +final class PickerNameProviderMacroTests: XCTestCase { + let testMacros: [String: Macro.Type] = [ + "PickerNameProvider": PickerNameProviderMacro.self + ] + + func testPickerNameProviderMacro() { + assertMacroExpansion( + """ + @PickerNameProvider + enum Character: Int { + case chelsea + case sky + } + """, + expandedSource: """ + + enum Character: Int { + case chelsea + case sky + } + + extension Character: CaseIterable { + } + + extension Character: Nameable { + var name: String { + switch self { + case .chelsea: + return "Chelsea" + case .sky: + return "Sky" + } + } + } + """, + macros: testMacros + ) + } + + func testPickerNameProviderMacroDiagnostics() { + assertMacroExpansion( + """ + @PickerNameProvider + struct Character { + } + """, + expandedSource: """ + + struct Character { + } + """, + diagnostics: [ + DiagnosticSpec(message: "@PickerNameProvider can only be applied to an 'enum'", line: 1, column: 1) + ], + macros: testMacros + ) + + assertMacroExpansion( + """ + @PickerNameProvider + enum Character { + case chelsea + case sky + } + """, + expandedSource: """ + + enum Character { + case chelsea + case sky + } + """, + diagnostics: [ + DiagnosticSpec(message: "@PickerNameProvider requires an Int backing", line: 1, column: 1) + ], + macros: testMacros + ) + } +} diff --git a/Tests/SwiftGodotMacrosTests/SceneTreeMacroTests.swift b/Tests/SwiftGodotMacrosTests/SceneTreeMacroTests.swift new file mode 100644 index 000000000..397673948 --- /dev/null +++ b/Tests/SwiftGodotMacrosTests/SceneTreeMacroTests.swift @@ -0,0 +1,91 @@ +// +// SceneTreeMacroTests.swift +// SwiftGodot +// +// Created by Marquis Kurt on 21/6/23. +// + +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest +import SwiftGodotMacroLibrary + +final class SceneTreeMacroTests: XCTestCase { + let testMacros: [String: Macro.Type] = [ + "SceneTree": SceneTreeMacro.self + ] + + func testMacroExpansion() { + assertMacroExpansion( + """ + class MyNode: Node { + @SceneTree(path: "Entities/CharacterBody2D") + var character: CharacterBody2D? + } + """, + expandedSource: """ + class MyNode: Node { + var character: CharacterBody2D? { + get { + getNodeOrNull(path: NodePath(stringLiteral: "Entities/CharacterBody2D")) as? CharacterBody2D + } + } + } + """, + macros: testMacros + ) + } + + func testMacroMissingPathDiagnostic() { + assertMacroExpansion( + """ + class MyNode: Node { + @SceneTree + var character: CharacterBody2D? + } + """, + expandedSource: """ + class MyNode: Node { + var character: CharacterBody2D? { + get { + getNodeOrNull(path: NodePath(stringLiteral: "")) as? Node + } + } + } + """, + diagnostics: [ + .init(message: "Missing argument 'path'", line: 2, column: 5) + ], + macros: testMacros + ) + } + + func testMacroNotOptionalDiagnostic() { + assertMacroExpansion( + """ + class MyNode: Node { + @SceneTree(path: "Entities/CharacterBody2D") + var character: CharacterBody2D + } + """, + expandedSource: """ + class MyNode: Node { + var character: CharacterBody2D { + get { + getNodeOrNull(path: NodePath(stringLiteral: "Entities/CharacterBody2D")) as? CharacterBody2D + } + } + } + """, + diagnostics: [ + .init(message: "Stored properties with SceneTree must be marked as Optional", + line: 2, + column: 5, + fixIts: [ + .init(message: "Mark as Optional") + ]) + ], + macros: testMacros + ) + } +} diff --git a/Tests/SwiftGodotMacrosTests/TextureLiteralMacroTests.swift b/Tests/SwiftGodotMacrosTests/TextureLiteralMacroTests.swift new file mode 100644 index 000000000..9e576ecf0 --- /dev/null +++ b/Tests/SwiftGodotMacrosTests/TextureLiteralMacroTests.swift @@ -0,0 +1,52 @@ +// +// TextureLiteralMacroTests.swift +// SwiftGodot +// +// Created by Marquis Kurt on 6/11/23. +// + +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest +import SwiftGodotMacroLibrary + +final class TextureLiteralMacroTests: XCTestCase { + let testMacros: [String: Macro.Type] = [ + "texture2DLiteral": Texture2DLiteralMacro.self + ] + + func testMacroExpansion() { + assertMacroExpansion( + """ + let spriteTexture = #texture2DLiteral("res://assets/icon.png") + """, + expandedSource: """ + let spriteTexture = { + guard let texture: Texture2D = GD.load(path: "res://assets/icon.png") else { + preconditionFailure( + "Texture could not be loaded.", + file: "TestModule/test.swift", + line: 1) + } + return texture + }() + """, + macros: testMacros + ) + } + + func testMacroExpansionFailure() { + assertMacroExpansion( + """ + let spriteTexture = #texture2DLiteral() + """, + expandedSource: """ + let spriteTexture = "" + """, + diagnostics: [ + .init(message: "Argument 'path' is missing.", line: 1, column: 21) + ], + macros: testMacros + ) + } +}