Skip to content

Commit

Permalink
Add macros from Indexing-Your-Heart/game
Browse files Browse the repository at this point in the history
Port over the macros meant to go upstream. As the sole author of the
macro library, I give permission to have these licensed under the same
license as the SwiftGodot project.
  • Loading branch information
alicerunsonfedora committed Sep 26, 2023
1 parent 844cc7c commit 4a80a61
Show file tree
Hide file tree
Showing 13 changed files with 801 additions and 4 deletions.
7 changes: 6 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
51 changes: 51 additions & 0 deletions Sources/SwiftGodotMacroLibrary/InitSwiftExtensionMacro.swift
Original file line number Diff line number Diff line change
@@ -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]
}
}
7 changes: 6 additions & 1 deletion Sources/SwiftGodotMacroLibrary/MacroGodot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
]
}
55 changes: 55 additions & 0 deletions Sources/SwiftGodotMacroLibrary/NativeHandleDiscardingMacro.swift
Original file line number Diff line number Diff line change
@@ -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)]
}
}
104 changes: 104 additions & 0 deletions Sources/SwiftGodotMacroLibrary/PickerNameProviderMacro.swift
Original file line number Diff line number Diff line change
@@ -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]
}
}
97 changes: 97 additions & 0 deletions Sources/SwiftGodotMacroLibrary/SceneTreeMacro.swift
Original file line number Diff line number Diff line change
@@ -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) }
"""
]
}
}
56 changes: 56 additions & 0 deletions Sources/SwiftGodotMacroLibrary/TextureLiteralMacro.swift
Original file line number Diff line number Diff line change
@@ -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
}()
"""
}
}
Loading

0 comments on commit 4a80a61

Please sign in to comment.