diff --git a/Generator/Generator/Enums.swift b/Generator/Generator/Enums.swift index a787316a3..028c53368 100644 --- a/Generator/Generator/Enums.swift +++ b/Generator/Generator/Enums.swift @@ -85,7 +85,7 @@ func generateEnums (_ p: Printer, cdef: JClassInfo?, values: [JGodotGlobalEnumEl } let extraConformances = enumDefName == "Error" ? ", Error" : "" - p ("public enum \(getGodotType (SimpleType (type: enumDefName))): Int64, CustomDebugStringConvertible\(extraConformances)") { + p ("public enum \(getGodotType (SimpleType (type: enumDefName))): Int64, CaseIterable, CustomDebugStringConvertible\(extraConformances)") { var used = Set () func getName (_ enumVal: JGodotValueElement) -> String? { diff --git a/Sources/SwiftGodot/SwiftGodot.docc/Exports.md b/Sources/SwiftGodot/SwiftGodot.docc/Exports.md index c8eef881b..2d1b9f876 100644 --- a/Sources/SwiftGodot/SwiftGodot.docc/Exports.md +++ b/Sources/SwiftGodot/SwiftGodot.docc/Exports.md @@ -288,3 +288,22 @@ To surface arrays in Godot, use a strong type for it, for example: @Export var myResources: VariantCollection ``` + +### Enumeration Values + +To surface enumeration values, use the `@Export(.enum)` marker on your variable, +and it is important that your enumeration conforms to `CaseIterable`, like this: + +``` +enum MyEnum: CaseIterable { + case first + case second +} + +@Godot +class Sample: Node { + @Export(.enum) + var myValue: MyEnum +} +``` + diff --git a/Sources/SwiftGodotMacroLibrary/MacroExport.swift b/Sources/SwiftGodotMacroLibrary/MacroExport.swift index 4d7dbbd0e..889214304 100644 --- a/Sources/SwiftGodotMacroLibrary/MacroExport.swift +++ b/Sources/SwiftGodotMacroLibrary/MacroExport.swift @@ -1,5 +1,5 @@ // -// File.swift +// MacroExport.swift // // // Created by Miguel de Icaza on 9/25/23. @@ -15,8 +15,17 @@ import SwiftSyntaxMacros public struct GodotExport: PeerMacro { - static func makeGetAccessor (varName: String, isOptional: Bool) -> String { + static func makeGetAccessor (varName: String, isOptional: Bool, isEnum: Bool) -> String { let name = "_mproxy_get_\(varName)" + if isEnum { + return + """ + func \(name) (args: [Variant]) -> Variant? { + return Variant (\(varName).rawValue) + } + """ + + } if isOptional { return """ @@ -35,11 +44,18 @@ public struct GodotExport: PeerMacro { } } - static func makeSetAccessor (varName: String, typeName: String, isOptional: Bool) -> String { + static func makeSetAccessor (varName: String, typeName: String, isOptional: Bool, isEnum: Bool) -> String { let name = "_mproxy_set_\(varName)" var body: String = "" - if typeName == "Variant" { + if isEnum { + body = + """ + if let iv = Int (args [0]), let ev = \(typeName)(rawValue: numericCast (iv)) { + self.\(varName) = ev + } + """ + } else if typeName == "Variant" { body = "\(varName) = args [0]" } else if godotVariants [typeName] == nil { let optBody = isOptional ? " else { \(varName) = nil }" : "" @@ -113,6 +129,14 @@ public struct GodotExport: PeerMacro { throw GodotMacroError.requiresNonOptionalGArrayCollection } + var isEnum = false + if case let .argumentList (arguments) = node.arguments, let expression = arguments.first?.expression { + isEnum = expression.description.trimmingCharacters(in: .whitespacesAndNewlines) == ".enum" + } + if isEnum && isOptional { + throw GodotMacroError.noSupportForOptionalEnums + + } var results: [DeclSyntax] = [] for singleVar in varDecl.bindings { @@ -162,8 +186,8 @@ public struct GodotExport: PeerMacro { results.append (DeclSyntax(stringLiteral: makeGArrayCollectionGetProxyAccessor(varName: varName, elementTypeName: elementTypeName))) results.append (DeclSyntax(stringLiteral: makeGArrayCollectionSetProxyAccessor(varName: varName, elementTypeName: elementTypeName))) } else if let typeName = type.as(IdentifierTypeSyntax.self)?.name.text { - results.append (DeclSyntax(stringLiteral: makeSetAccessor(varName: varName, typeName: typeName, isOptional: isOptional))) - results.append (DeclSyntax(stringLiteral: makeGetAccessor(varName: varName, isOptional: isOptional))) + results.append (DeclSyntax(stringLiteral: makeSetAccessor(varName: varName, typeName: typeName, isOptional: isOptional, isEnum: isEnum))) + results.append (DeclSyntax(stringLiteral: makeGetAccessor(varName: varName, isOptional: isOptional, isEnum: isEnum))) } } diff --git a/Sources/SwiftGodotMacroLibrary/MacroGodot.swift b/Sources/SwiftGodotMacroLibrary/MacroGodot.swift index f2eac8602..06a1104a1 100644 --- a/Sources/SwiftGodotMacroLibrary/MacroGodot.swift +++ b/Sources/SwiftGodotMacroLibrary/MacroGodot.swift @@ -204,9 +204,11 @@ class GodotMacroProcessor { ctor.append ("\tclassInfo.registerMethod(name: StringName(\"\(funcName)\"), flags: .default, returnValue: \(retProp ?? "nil"), arguments: \(funcArgs == "" ? "[]" : "\(funcName)Args"), function: \(className)._mproxy_\(funcName))\n") } - func processVariable (_ varDecl: VariableDeclSyntax, prefix: String?) throws { + // Returns true if it used "tryCase" + func processVariable (_ varDecl: VariableDeclSyntax, prefix: String?) throws -> Bool { + var usedTryCase = false guard hasExportAttribute(varDecl.attributes) else { - return + return false } guard let last = varDecl.bindings.last else { throw GodotMacroError.noVariablesFound @@ -232,6 +234,13 @@ class GodotMacroProcessor { guard let ips = singleVar.pattern.as(IdentifierPatternSyntax.self) else { throw GodotMacroError.expectedIdentifier(singleVar) } + guard let last = varDecl.bindings.last else { + throw GodotMacroError.noVariablesFound + } + guard let ta = last.typeAnnotation?.type.description.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) else { + throw GodotMacroError.noTypeFound(varDecl) + } + let varNameWithPrefix = ips.identifier.text let varNameWithoutPrefix = String(varNameWithPrefix.trimmingPrefix(prefix ?? "")) let proxySetterName = "_mproxy_set_\(varNameWithPrefix)" @@ -275,8 +284,16 @@ class GodotMacroProcessor { } } } - let propType = godotTypeToProp (typeName: typeName) + let mappedType = godotTypeToProp (typeName: typeName) let pinfo = "_p\(varNameWithPrefix)" + let isEnum = firstLabeledExpression?.description == "enum" + + + let propType = isEnum ? ".int" : mappedType + let fallback = isEnum ? "tryCase (\(ta).self)" : "\"\"" + if isEnum { + usedTryCase = true + } ctor.append ( """ let \(pinfo) = PropInfo ( @@ -284,7 +301,7 @@ class GodotMacroProcessor { propertyName: "\(varNameWithPrefix)", className: className, hint: .\(firstLabeledExpression?.description ?? "none"), - hintStr: \(secondLabeledExpression?.description ?? "\"\""), + hintStr: \(secondLabeledExpression?.description ?? fallback), usage: .default) """) @@ -293,6 +310,10 @@ class GodotMacroProcessor { ctor.append("\tclassInfo.registerMethod (name: \"\(setterName)\", flags: .default, returnValue: nil, arguments: [\(pinfo)], function: \(className).\(proxySetterName))\n") ctor.append("\tclassInfo.registerProperty (\(pinfo), getter: \"\(getterName)\", setter: \"\(setterName)\")\n") } + if usedTryCase { + return true + } + return false } func processGArrayCollectionVariable(_ varDecl: VariableDeclSyntax, prefix: String?) throws { @@ -402,7 +423,7 @@ class GodotMacroProcessor { """ var previousGroupPrefix: String? = nil var previousSubgroupPrefix: String? = nil - + var needTrycase = false for member in classDecl.memberBlock.members.enumerated() { let decl = member.element.decl @@ -420,12 +441,23 @@ class GodotMacroProcessor { if varDecl.isGArrayCollection { try processGArrayCollectionVariable(varDecl, prefix: previousSubgroupPrefix ?? previousGroupPrefix) } else { - try processVariable(varDecl, prefix: previousSubgroupPrefix ?? previousGroupPrefix) + if try processVariable(varDecl, prefix: previousSubgroupPrefix ?? previousGroupPrefix) { + needTrycase = true + } } } else if let macroDecl = MacroExpansionDeclSyntax(decl) { try classInitSignals(macroDecl) } } + if needTrycase { + ctor.append ( + """ + func tryCase (_ type: T.Type) -> GString { + GString (type.allCases.map { v in "\\(v):\\(v.rawValue)" }.joined(separator: ",")) + } + func tryCase (_ type: T.Type) -> String { "" } + """) + } ctor.append("} ()\n") return ctor } diff --git a/Sources/SwiftGodotMacroLibrary/MacroSharedApi.swift b/Sources/SwiftGodotMacroLibrary/MacroSharedApi.swift index 3d2ecb95c..6fded4c38 100644 --- a/Sources/SwiftGodotMacroLibrary/MacroSharedApi.swift +++ b/Sources/SwiftGodotMacroLibrary/MacroSharedApi.swift @@ -48,6 +48,7 @@ enum GodotMacroError: Error, DiagnosticMessage { case expectedIdentifier(PatternBindingListSyntax.Element) case unknownError(Error) case unsupportedCallableEffect + case noSupportForOptionalEnums var severity: DiagnosticSeverity { return .error @@ -77,6 +78,8 @@ enum GodotMacroError: Error, DiagnosticMessage { "@Export optional Collections are not supported" case .unsupportedCallableEffect: "@Callable does not support asynchronous or throwing functions" + case .noSupportForOptionalEnums: + "@Export(.enum) does not support optional values for the enumeration" } } diff --git a/Sources/SwiftGodotMacroLibrary/PickerNameProviderMacro.swift b/Sources/SwiftGodotMacroLibrary/PickerNameProviderMacro.swift index e95f7be63..e1f9f5d5b 100644 --- a/Sources/SwiftGodotMacroLibrary/PickerNameProviderMacro.swift +++ b/Sources/SwiftGodotMacroLibrary/PickerNameProviderMacro.swift @@ -28,7 +28,7 @@ public struct PickerNameProviderMacro: ExtensionMacro { case .notAnEnum: return "@PickerNameProvider can only be applied to an 'enum'" case .missingInt: - return "@PickerNameProvider requires an Int backing" + return "@PickerNameProvider requires an Int64 backing" } } @@ -58,12 +58,6 @@ public struct PickerNameProviderMacro: ExtensionMacro { 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 } diff --git a/Tests/SwiftGodotMacrosTests/MacroGodotExportEnumTests.swift b/Tests/SwiftGodotMacrosTests/MacroGodotExportEnumTests.swift new file mode 100644 index 000000000..8505d8285 --- /dev/null +++ b/Tests/SwiftGodotMacrosTests/MacroGodotExportEnumTests.swift @@ -0,0 +1,111 @@ +// +// MacroGodotExportEnumTests.swift +// SwiftGodotMacrosTests +// +// Created by Estevan Hernandez on 11/29/23. +// + +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest +import SwiftGodotMacroLibrary + +final class MacroGodotExportEnumTests: XCTestCase { + let testMacros: [String: Macro.Type] = [ + "Godot": GodotMacro.self, + "Export": GodotExport.self, + ] + + func testExportEnumGodot() { + assertMacroExpansion( + """ + enum Demo: Int, CaseIterable { + case first + } + enum Demo64: Int64, CaseIterable { + case first + } + @Godot + class SomeNode: Node { + @Export(.enum) var demo: Demo + @Export(.enum) var demo64: Demo64 + } + """, + expandedSource: + """ + enum Demo: Int, CaseIterable { + case first + } + enum Demo64: Int64, CaseIterable { + case first + } + class SomeNode: Node { + var demo: Demo + + func _mproxy_set_demo (args: [Variant]) -> Variant? { + if let iv = Int (args [0]), let ev = Demo(rawValue: numericCast (iv)) { + self.demo = ev + } + return nil + } + + func _mproxy_get_demo (args: [Variant]) -> Variant? { + return Variant (demo.rawValue) + } + var demo64: Demo64 + + func _mproxy_set_demo64 (args: [Variant]) -> Variant? { + if let iv = Int (args [0]), let ev = Demo64(rawValue: numericCast (iv)) { + self.demo64 = ev + } + return nil + } + + func _mproxy_get_demo64 (args: [Variant]) -> Variant? { + return Variant (demo64.rawValue) + } + + override open class var classInitializer: Void { + let _ = super.classInitializer + return _initializeClass + } + + private static var _initializeClass: Void = { + let className = StringName("SomeNode") + assert(ClassDB.classExists(class: className)) + let classInfo = ClassInfo (name: className) + let _pdemo = PropInfo ( + propertyType: .int, + propertyName: "demo", + className: className, + hint: .enum, + hintStr: tryCase (Demo.self), + usage: .default) + classInfo.registerMethod (name: "_mproxy_get_demo", flags: .default, returnValue: _pdemo, arguments: [], function: SomeNode._mproxy_get_demo) + classInfo.registerMethod (name: "_mproxy_set_demo", flags: .default, returnValue: nil, arguments: [_pdemo], function: SomeNode._mproxy_set_demo) + classInfo.registerProperty (_pdemo, getter: "_mproxy_get_demo", setter: "_mproxy_set_demo") + let _pdemo64 = PropInfo ( + propertyType: .int, + propertyName: "demo64", + className: className, + hint: .enum, + hintStr: tryCase (Demo64.self), + usage: .default) + classInfo.registerMethod (name: "_mproxy_get_demo64", flags: .default, returnValue: _pdemo64, arguments: [], function: SomeNode._mproxy_get_demo64) + classInfo.registerMethod (name: "_mproxy_set_demo64", flags: .default, returnValue: nil, arguments: [_pdemo64], function: SomeNode._mproxy_set_demo64) + classInfo.registerProperty (_pdemo64, getter: "_mproxy_get_demo64", setter: "_mproxy_set_demo64") + func tryCase (_ type: T.Type) -> GString { + GString (type.allCases.map { v in + "\\(v):\\(v.rawValue)" + } .joined(separator: ",")) + } + func tryCase (_ type: T.Type) -> String { + "" + } + } () + } +""", + macros: testMacros + ) + } +} diff --git a/Tests/SwiftGodotMacrosTests/PickerNameProviderMacroTests.swift b/Tests/SwiftGodotMacrosTests/PickerNameProviderMacroTests.swift index ddfde702c..3e82e7f70 100644 --- a/Tests/SwiftGodotMacrosTests/PickerNameProviderMacroTests.swift +++ b/Tests/SwiftGodotMacrosTests/PickerNameProviderMacroTests.swift @@ -19,14 +19,23 @@ final class PickerNameProviderMacroTests: XCTestCase { assertMacroExpansion( """ @PickerNameProvider - enum Character: Int { + enum Character: Int64 { + case chelsea + case sky + } + @PickerNameProvider + enum Character2: Int { case chelsea case sky } """, expandedSource: """ - enum Character: Int { + enum Character: Int64 { + case chelsea + case sky + } + enum Character2: Int { case chelsea case sky } @@ -44,6 +53,20 @@ final class PickerNameProviderMacroTests: XCTestCase { } } } + + extension Character2: CaseIterable { + } + + extension Character2: Nameable { + var name: String { + switch self { + case .chelsea: + return "Chelsea" + case .sky: + return "Sky" + } + } + } """, macros: testMacros ) @@ -66,26 +89,5 @@ final class PickerNameProviderMacroTests: XCTestCase { ], 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 - ) } }