Skip to content

Commit 6f6c5dc

Browse files
committed
Create DeclGroupProtocol to reduce duplication, and introduce property extractor with 'basic' type inference
Properties can be defined in so many ways in Swift, and my new Property API intends to abstract over that so you don't have to worry about all the edge cases anymore
1 parent 106daeb commit 6f6c5dc

File tree

12 files changed

+401
-54
lines changed

12 files changed

+401
-54
lines changed
Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,33 @@
11
import SwiftSyntax
22

3+
// TODO: Enable initializing from an `any DeclGroupSyntax`.
34
/// Wraps a declaration group (a declaration with a scoped block of members).
45
/// For example an `enum` or a `struct` etc.
5-
public struct DeclGroup {
6-
/// The underlying syntax node.
7-
public var _syntax: DeclGroupSyntax
6+
public struct DeclGroup<WrappedSyntax: DeclGroupSyntax>: DeclGroupProtocol {
7+
public var _syntax: WrappedSyntax
88

9-
/// Wraps a declaration group syntax node.
10-
public init(_ syntax: DeclGroupSyntax) {
9+
public init(_ syntax: WrappedSyntax) {
1110
_syntax = syntax
1211
}
1312

14-
/// Gets whether the declaration has the `public` access modifier.
15-
public var isPublic: Bool {
16-
_syntax.isPublic
13+
public var identifier: String {
14+
if let `struct` = asStruct {
15+
`struct`.identifier
16+
} else if let `enum` = asEnum {
17+
`enum`.identifier
18+
} else {
19+
// TODO: Implement wrappers for all other decl group types.
20+
fatalError("Unhandled decl group type '\(type(of: _syntax))'")
21+
}
1722
}
1823

19-
/// Gets all of the declaration group's member declarations.
20-
public var members: [Decl] {
21-
_syntax.memberBlock.members.map(\.decl).map(Decl.init)
24+
/// Gets the decl group as a struct if it's a struct.
25+
public var asStruct: Struct? {
26+
Struct(_syntax)
27+
}
28+
29+
/// Gets the decl group as an enum if it's an enum.
30+
public var asEnum: Enum? {
31+
Enum(_syntax)
2232
}
2333
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import SwiftSyntax
2+
3+
/// A declaration group (e.g. a `struct` or `class` rather than a regular
4+
/// declaration such as `var`).
5+
public protocol DeclGroupProtocol {
6+
/// The type of the underlying syntax node being wrapped.
7+
associatedtype WrappedSyntax: DeclGroupSyntax
8+
/// The underlying syntax node.
9+
var _syntax: WrappedSyntax { get }
10+
/// The declaration's identifier.
11+
///
12+
/// For some reason SwiftSyntax's `DeclGroupSyntax` protocol doesn't have the
13+
/// declaration's identifier, so this needs to be implemented manually
14+
/// for every declaration wrapper. Maybe due to extensions technically not
15+
/// having a name? (although they're always attached to a specific identifier).
16+
var identifier: String { get }
17+
/// Wraps a syntax node.
18+
init(_ syntax: WrappedSyntax)
19+
20+
}
21+
22+
extension DeclGroupProtocol {
23+
/// Attempts to initialize the wrapper from an arbitrary decl group (succeeds
24+
/// if the decl group is the right type of syntax).
25+
public init?(_ syntax: any DeclGroupSyntax) {
26+
guard let syntax = syntax.as(WrappedSyntax.self) else {
27+
return nil
28+
}
29+
self.init(syntax)
30+
}
31+
32+
/// The declaration group's members.
33+
public var members: [Decl] {
34+
_syntax.memberBlock.members.map(\.decl).map(Decl.init)
35+
}
36+
37+
/// The declaration group's declared properties.
38+
public var properties: [Property] {
39+
members.compactMap(\.asVariable).flatMap { variable in
40+
var bindings = variable._syntax.bindings.flatMap { binding in
41+
Property.properties(from: binding)
42+
}
43+
// For the declaration `var a, b: Int` where `a` doesn't have an annotation,
44+
// `a` gets given the type of `b` (`Int`). To implement this, we 'drag' the
45+
// type annotations backwards over the non-annotated bindings.
46+
var lastSeenType: Type?
47+
for (i, binding) in bindings.enumerated().reversed() {
48+
if let type = binding.type {
49+
lastSeenType = type
50+
} else {
51+
bindings[i].type = lastSeenType
52+
}
53+
}
54+
return bindings
55+
}
56+
}
57+
58+
/// The types inherited from or conformed to by the decl group. Doesn't
59+
/// include conformances added by other declaration groups such as an
60+
/// `extension` of the current declaration.
61+
public var inheritedTypes: [Type] {
62+
_syntax.inheritanceClause?.inheritedTypes.map(\.type).map(Type.init) ?? []
63+
}
64+
65+
// TODO: Replace this with an accessLevel property
66+
/// Whether the declaration was declared with the `public` access level
67+
/// modifier.
68+
public var isPublic: Bool {
69+
_syntax.isPublic
70+
}
71+
}

Sources/MacroToolkit/Enum.swift

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,18 @@
11
import SwiftSyntax
22

3-
/// Wraps an enum declaration.
4-
public struct Enum {
3+
/// Wraps an `enum` declaration.
4+
public struct Enum: DeclGroupProtocol {
55
public var _syntax: EnumDeclSyntax
6-
7-
public init?(_ syntax: any DeclGroupSyntax) {
8-
guard let syntax = syntax.as(EnumDeclSyntax.self) else {
9-
return nil
10-
}
11-
_syntax = syntax
6+
7+
public var identifier: String {
8+
_syntax.name.withoutTrivia().text
129
}
1310

1411
public init(_ syntax: EnumDeclSyntax) {
1512
_syntax = syntax
1613
}
1714

18-
public var identifier: String {
19-
_syntax.name.withoutTrivia().text
20-
}
21-
15+
/// The `enum`'s cases.
2216
public var cases: [EnumCase] {
2317
_syntax.memberBlock.members
2418
.compactMap { member in
@@ -28,8 +22,4 @@ public struct Enum {
2822
syntax.elements.map(EnumCase.init)
2923
}
3024
}
31-
32-
public var isPublic: Bool {
33-
_syntax.isPublic
34-
}
3525
}

Sources/MacroToolkit/Expr.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import SwiftSyntax
22

3+
// TODO: Introduce a generic `NumericLiteral` type for both integer and floating point
4+
// literals, often macro devs might forget to check for integer literals if they just
5+
// want a floating point number.
36
// TODO: Create wrapper for tuple syntax
47
/// Wraps an expression syntax node.
58
public struct Expr {

Sources/MacroToolkit/ExprProtocol.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public protocol ExprProtocol {
1313
extension ExprProtocol {
1414
/// Attempts to initialize the wrapper from an arbitrary expression (succeeds
1515
/// if the expression is the right type of syntax).
16-
public init?(_ syntax: ExprSyntaxProtocol) {
16+
public init?(_ syntax: any ExprSyntaxProtocol) {
1717
guard let syntax = syntax.as(WrappedSyntax.self) else {
1818
return nil
1919
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import SwiftSyntax
2+
3+
/// A property of a declaration group such as a `struct`.
4+
public struct Property {
5+
public var _syntax: TokenSyntax
6+
public var identifier: String
7+
public var type: Type?
8+
public var initialValue: Expr?
9+
public var accessors: [AccessorDeclSyntax]
10+
11+
public var getter: AccessorDeclSyntax? {
12+
accessors.first { $0.accessorSpecifier.tokenKind == .keyword(.get) }
13+
}
14+
15+
public var setter: AccessorDeclSyntax? {
16+
accessors.first { $0.accessorSpecifier.tokenKind == .keyword(.set) }
17+
}
18+
19+
public var isStored: Bool {
20+
getter == nil
21+
}
22+
23+
static func properties(from binding: PatternBindingSyntax) -> [Property] {
24+
let accessors: [AccessorDeclSyntax] = switch binding.accessorBlock?.accessors {
25+
case .accessors(let block):
26+
Array(block)
27+
case .getter(let getter):
28+
[AccessorDeclSyntax(accessorSpecifier: .keyword(.get)) { getter }]
29+
case .none:
30+
[]
31+
}
32+
return properties(
33+
pattern: binding.pattern,
34+
initialValue: (binding.initializer?.value).map(Expr.init),
35+
type: (binding.typeAnnotation?.type).map(Type.init),
36+
accessors: accessors
37+
)
38+
}
39+
40+
private static func properties(
41+
pattern: PatternSyntax,
42+
initialValue: Expr?,
43+
type: Type?,
44+
accessors: [AccessorDeclSyntax]
45+
) -> [Property] {
46+
switch pattern.asProtocol(PatternSyntaxProtocol.self) {
47+
case let pattern as IdentifierPatternSyntax:
48+
let type: Type? = if let type {
49+
type
50+
} else {
51+
if initialValue?.asIntegerLiteral != nil {
52+
Type("Int")
53+
} else if initialValue?.asFloatLiteral != nil {
54+
Type("Double")
55+
} else if initialValue?.asStringLiteral != nil {
56+
Type("String")
57+
} else if initialValue?.asBooleanLiteral != nil {
58+
Type("Bool")
59+
} else if initialValue?.asRegexLiteral != nil {
60+
Type("Regex")
61+
} else if let array = initialValue?._syntax.as(ArrayExprSyntax.self) {
62+
inferArrayLiteralType(array)
63+
} else {
64+
nil
65+
}
66+
}
67+
return [
68+
Property(
69+
_syntax: pattern.identifier,
70+
identifier: pattern.identifier.text,
71+
type: type,
72+
initialValue: initialValue,
73+
accessors: accessors
74+
)
75+
]
76+
case let pattern as TuplePatternSyntax:
77+
let tupleInitialValue: TupleExprSyntax? =
78+
if let initialValue, let tuple = initialValue._syntax.as(TupleExprSyntax.self),
79+
tuple.elements.count == pattern.elements.count
80+
{
81+
tuple
82+
} else {
83+
nil
84+
}
85+
let tupleType: TupleType? =
86+
if let type,
87+
let tuple = TupleType(type),
88+
tuple.elements.count == pattern.elements.count
89+
{
90+
tuple
91+
} else {
92+
nil
93+
}
94+
return pattern.elements.enumerated().flatMap { (index, element) in
95+
let initialValue = if let tupleInitialValue {
96+
Expr(Array(tupleInitialValue.elements)[index].expression)
97+
} else {
98+
initialValue.map { expr in
99+
Expr(
100+
MemberAccessExprSyntax(
101+
leadingTrivia: nil, base: expr._syntax.parenthesized,
102+
period: .periodToken(),
103+
name: .identifier(String(index)), trailingTrivia: nil
104+
)
105+
)
106+
}
107+
}
108+
109+
// If in a tuple initial value expression, an empty array literal is inferred to have
110+
// type `Array<Any>`, unlike with regular initial value expressions.
111+
let type =
112+
if let arrayLiteral = initialValue?._syntax.as(ArrayExprSyntax.self),
113+
arrayLiteral.elements.isEmpty
114+
{
115+
Type("Array<Any>")
116+
} else {
117+
tupleType?.elements[index]
118+
}
119+
120+
// Tuple bindings can't have accessors
121+
return properties(
122+
pattern: element.pattern, initialValue: initialValue, type: type,
123+
accessors: []
124+
)
125+
}
126+
case _ as WildcardPatternSyntax:
127+
return []
128+
default:
129+
// TODO: Handle all patterns
130+
return []
131+
}
132+
}
133+
134+
private static func inferArrayLiteralType(_ arrayLiteral: ArrayExprSyntax) -> Type? {
135+
var elementType: String?
136+
for element in arrayLiteral.elements {
137+
if element.expression.is(IntegerLiteralExprSyntax.self) {
138+
if elementType == nil {
139+
elementType = "Int"
140+
} else if elementType == "Double" {
141+
continue
142+
} else {
143+
return nil
144+
}
145+
} else if element.expression.is(FloatLiteralExprSyntax.self) {
146+
if elementType == nil || elementType == "Int" {
147+
elementType = "Double"
148+
} else {
149+
return nil
150+
}
151+
} else if element.expression.is(BooleanLiteralExprSyntax.self) {
152+
if elementType == nil {
153+
elementType = "Bool"
154+
} else {
155+
return nil
156+
}
157+
} else if element.expression.is(StringLiteralExprSyntax.self) {
158+
if elementType == nil {
159+
elementType = "String"
160+
} else {
161+
return nil
162+
}
163+
} else if element.expression.is(RegexLiteralExprSyntax.self) {
164+
if elementType == nil {
165+
elementType = "Regex"
166+
} else {
167+
return nil
168+
}
169+
}
170+
}
171+
172+
return if let elementType {
173+
Type("Array<\(raw: elementType)>")
174+
} else {
175+
nil
176+
}
177+
}
178+
}

Sources/MacroToolkit/Struct.swift

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,14 @@
11
import SwiftSyntax
22

33
/// Wraps a `struct` declaration.
4-
public struct Struct {
5-
/// The underlying syntax node.
4+
public struct Struct: DeclGroupProtocol {
65
public var _syntax: StructDeclSyntax
76

8-
/// Wraps a syntax node.
9-
public init(_ syntax: StructDeclSyntax) {
10-
_syntax = syntax
7+
public var identifier: String {
8+
_syntax.name.withoutTrivia().text
119
}
1210

13-
/// Attempts to get a declaration group as a struct declaration.
14-
public init?(_ syntax: any DeclGroupSyntax) {
15-
guard let syntax = syntax.as(StructDeclSyntax.self) else {
16-
return nil
17-
}
11+
public init(_ syntax: StructDeclSyntax) {
1812
_syntax = syntax
1913
}
20-
21-
// TODO: Add members property to all declgroupsyntax decls through protocol default impl
22-
/// The struct's members.
23-
public var members: [Decl] {
24-
_syntax.memberBlock.members.map(\.decl).map(Decl.init)
25-
}
26-
27-
/// Types that the struct conforms to.
28-
public var inheritedTypes: [Type] {
29-
_syntax.inheritanceClause?.inheritedTypes.map(\.type).map(Type.init) ?? []
30-
}
31-
32-
/// Whether the `struct` was declared with the `public` access level modifier.
33-
public var isPublic: Bool {
34-
_syntax.isPublic
35-
}
3614
}

0 commit comments

Comments
 (0)