Skip to content

Commit

Permalink
Implement Functionality for Structs
Browse files Browse the repository at this point in the history
  • Loading branch information
ugommirikwe committed Jun 20, 2024
1 parent ab14d16 commit 9726bfa
Show file tree
Hide file tree
Showing 6 changed files with 429 additions and 48 deletions.
14 changes: 14 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"pins" : [
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-syntax.git",
"state" : {
"revision" : "64889f0c732f210a935a0ad7cda38f77f876262d",
"version" : "509.1.1"
}
}
],
"version" : 2
}
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ let package = Package(
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "CopyableMacro",
targets: ["CopyableMacro"]
targets: ["CopyableMacroMacros"]
),
.executable(
name: "CopyableMacroClient",
Expand Down
27 changes: 21 additions & 6 deletions Sources/CopyableMacro/CopyableMacro.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
// The Swift Programming Language
// https://docs.swift.org/swift-book

/// A macro that produces both a value and a string containing the
/// source code that generated the value. For example,
/// A macro that adds a `copy()` method into a type bearing the annotation. For example:
///
/// #stringify(x + y)
/// @Copyable
/// struct User {
/// let name: String
/// let age: Int
/// }
///
/// produces a tuple `(x + y, "x + y")`.
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "CopyableMacroMacros", type: "StringifyMacro")
/// updates the struct as follows:
///
/// struct User {
/// let name: String
/// let age: Int
///
/// func copy(name: String? = nil, age: Int? = nil) -> Self {
/// .init(
/// name: name ?? self.name
/// age: age ?? self.age
/// )
/// }
/// }
@attached(member, names: named(copy))
public macro Copyable() = #externalMacro(module: "CopyableMacroMacros", type: "CopyableMacroMacro")
30 changes: 26 additions & 4 deletions Sources/CopyableMacroClient/main.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,30 @@
import CopyableMacro

let a = 17
let b = 25
@Copyable
final class Sample {
var x: Int
let y: Double

let (result, code) = #stringify(a + b)
init(x: Int, y: Double) {
self.x = x
self.y = y
}
}

print("The value \(result) was produced by the code \"\(code)\"")
let sample = Sample(x: 1, y: 2)

@Copyable
struct StructSample {
var firstProp: Bool
let secondProp: String
//let t: String? = nil
var fourth: String {
secondProp
}
let fifth: [Int]

}

let structSample = StructSample(firstProp: true, secondProp: "secondProp", fifth: [0, 1])

print(sample)
145 changes: 129 additions & 16 deletions Sources/CopyableMacroMacros/CopyableMacroMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,144 @@ import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros

/// Implementation of the `stringify` macro, which takes an expression
/// of any type and produces a tuple containing the value of that expression
/// and the source code that produced the value. For example
/// Implementation of the `Copyable` macro, adds a `copy` method to a type.
///
/// #stringify(x + y)
/// For example
///
/// @Copyable
///
/// will expand to
///
/// (x + y, "x + y")
public struct StringifyMacro: ExpressionMacro {
/// ... TODO
public struct CopyableMacroMacro: MemberMacro {
@main
struct CopyableMacroPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
CopyableMacroMacro.self,
]
}

public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) -> ExprSyntax {
guard let argument = node.argumentList.first?.expression else {
fatalError("compiler bug: the macro does not have any arguments")
) throws -> [DeclSyntax] {
// TODO: Extract the properties from the type--must be a class or struct
//let storedProperties = declaration
// .as(ClassDeclSyntax.self)?.storedProperties() ?? declaration
// .as(StructDeclSyntax.self)?.storedProperties()

// Extract the properties from the type--must be a struct
guard let storedProperties = declaration
.as(StructDeclSyntax.self)?.storedProperties(),
!storedProperties.isEmpty
else { return [] }

let funcArguments = storedProperties
.compactMap { property -> (name: String, type: String)? in
guard
// Get the property's name (a.k.a. identifier)...
let patternBinding = property.bindings.first?.as(
PatternBindingSyntax.self
),

let name = patternBinding.pattern.as(
IdentifierPatternSyntax.self
)?.identifier,

// ...and then the property's type...
let type = /*patternBinding.typeAnnotation?.as(
TypeAnnotationSyntax.self
)?.type.as(
IdentifierTypeSyntax.self
)?.name ??
// ... including if it's an optional type
patternBinding.typeAnnotation?.as(
TypeAnnotationSyntax.self
)?.type.as(
OptionalTypeSyntax.self
)?.wrappedType.as(
IdentifierTypeSyntax.self
)?.name ??*/

patternBinding.typeAnnotation?.as(TypeAnnotationSyntax.self)?.trimmed.description.replacingOccurrences(of: "?", with: "")
else { return nil }

return (name: name.text, type: type)
}

let funcBody: ExprSyntax = """
.init(
\(raw: funcArguments.map { "\($0.name): \($0.name) ?? self.\($0.name)" }.joined(separator: ", \n"))
)
"""

guard
let funcDeclSyntax = try? FunctionDeclSyntax(
SyntaxNodeString(
stringLiteral: """
public func copy(
\(funcArguments.map { "\($0.name)\($0.type)? = nil" }.joined(separator: ", \n"))
) -> Self
""".trimmingCharacters(in: .whitespacesAndNewlines)
),
bodyBuilder: {
funcBody
}
),
let finalDeclaration = DeclSyntax(funcDeclSyntax)
else {
return []
}

return [finalDeclaration]
}


}

return "(\(argument), \(literal: argument.description))"
extension VariableDeclSyntax {
/// Check this variable is a stored property
var isStoredProperty: Bool {
guard let binding = bindings.first,
bindings.count == 1,
modifiers.contains(where: {
$0.name == .keyword(.public)
}) || modifiers.isEmpty
else { return false }

switch binding.accessorBlock?.accessors {
case .none:
return true

case .accessors(let node):
for accessor in node {
switch accessor.accessorSpecifier.tokenKind {
case .keyword(.willSet), .keyword(.didSet):
// stored properties can have observers
break
default:
// everything else makes it a computed property
return false
}
}
return true

case .getter:
return false
}
}
}

@main
struct CopyableMacroPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
StringifyMacro.self,
]
extension DeclGroupSyntax {
/// Get the stored properties from the declaration based on syntax.
func storedProperties() -> [VariableDeclSyntax] {
memberBlock.members.compactMap { member in
guard let variable = member.decl.as(VariableDeclSyntax.self),
variable.isStoredProperty
else { return nil }

return variable
}
}
}
Loading

0 comments on commit 9726bfa

Please sign in to comment.