Skip to content

Commit 901f1c9

Browse files
authored
Merge pull request #3298 from calda/cal--computed-var-body-macro
2 parents 33e65f8 + 3f3e923 commit 901f1c9

7 files changed

Lines changed: 238 additions & 8 deletions

File tree

Sources/SwiftCompilerPluginMessageHandling/PluginMacroExpansionContext.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ class ParsedSyntaxRegistry {
6565
return Syntax(PatternSyntax.parse(from: &parser))
6666
case .attribute:
6767
return Syntax(AttributeSyntax.parse(from: &parser))
68+
case .accessor:
69+
return Syntax(AccessorDeclSyntax.parse(from: &parser))
6870
}
6971
}
7072

Sources/SwiftCompilerPluginMessageHandling/PluginMessages.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ public enum PluginMessage {
256256
case type
257257
case pattern
258258
case attribute
259+
case accessor
259260
}
260261
public var kind: Kind
261262
public var source: String

Sources/SwiftSyntaxMacroExpansion/MacroExpansion.swift

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@
1212

1313
#if compiler(>=6)
1414
import SwiftBasicFormat
15+
import SwiftDiagnostics
16+
import SwiftIfConfig
1517
public import SwiftSyntax
1618
@_spi(MacroExpansion) @_spi(ExperimentalLanguageFeatures) public import SwiftSyntaxMacros
1719
#else
1820
import SwiftBasicFormat
21+
import SwiftDiagnostics
22+
import SwiftIfConfig
1923
import SwiftSyntax
2024
@_spi(MacroExpansion) @_spi(ExperimentalLanguageFeatures) import SwiftSyntaxMacros
2125
#endif
@@ -394,6 +398,38 @@ public func expandAttachedMacroWithoutCollapsing<Context: MacroExpansionContext>
394398
providingBodyFor: declToPass,
395399
in: context
396400
)
401+
} else if let varDecl = node.as(VariableDeclSyntax.self),
402+
varDecl.bindings.count == 1,
403+
let binding = varDecl.bindings.first,
404+
let accessorBlock = binding.accessorBlock,
405+
case .getter(let stmts) = accessorBlock.accessors
406+
{
407+
// Create an implicit `AccessorDeclSyntax` to use for the macro expansion
408+
// of this computed var decl
409+
let getterDecl = AccessorDeclSyntax(
410+
accessorSpecifier: .keyword(.get, presence: .missing),
411+
body: CodeBlockSyntax(
412+
leftBrace: accessorBlock.leftBrace,
413+
statements: stmts,
414+
rightBrace: accessorBlock.rightBrace
415+
)
416+
)
417+
418+
// Insert the enclosing `PatternBindingSyntax`'s var decl context
419+
// into the lexical context
420+
var context: MacroExpansionContext = context
421+
if let varDeclLexicalContext = binding.asMacroLexicalContext() {
422+
context = PrependLexicalContextWrapperContext(
423+
prependLexicalContext: [varDeclLexicalContext],
424+
wrapping: context
425+
)
426+
}
427+
428+
body = try attachedMacro.expansion(
429+
of: attributeNode,
430+
providingBodyFor: getterDecl,
431+
in: context
432+
)
397433
} else {
398434
// Compiler error: declaration must have a body.
399435
throw MacroExpansionError.declarationHasNoBody
@@ -635,3 +671,39 @@ public func collapse<Node: SyntaxProtocol>(
635671

636672
return collapsed
637673
}
674+
675+
/// Wrapper context that prepends additional `lexicalContext`
676+
/// to an existing `MacroExpansionContext`
677+
final class PrependLexicalContextWrapperContext: MacroExpansionContext {
678+
init(prependLexicalContext: [Syntax], wrapping wrappedContext: MacroExpansionContext) {
679+
self.prependLexicalContext = prependLexicalContext
680+
self.wrappedContext = wrappedContext
681+
}
682+
683+
let prependLexicalContext: [Syntax]
684+
let wrappedContext: MacroExpansionContext
685+
686+
var lexicalContext: [Syntax] {
687+
prependLexicalContext + wrappedContext.lexicalContext
688+
}
689+
690+
func makeUniqueName(_ name: String) -> TokenSyntax {
691+
wrappedContext.makeUniqueName(name)
692+
}
693+
694+
func diagnose(_ diagnostic: Diagnostic) {
695+
wrappedContext.diagnose(diagnostic)
696+
}
697+
698+
func location(
699+
of node: some SyntaxProtocol,
700+
at position: PositionInSyntaxNode,
701+
filePathMode: SourceLocationFilePathMode
702+
) -> AbstractSourceLocation? {
703+
wrappedContext.location(of: node, at: position, filePathMode: filePathMode)
704+
}
705+
706+
var buildConfiguration: (any BuildConfiguration)? {
707+
wrappedContext.buildConfiguration
708+
}
709+
}

Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,32 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
707707
let declNodeWithBody = nodeWithBody as? any DeclSyntaxProtocol & WithOptionalCodeBlockSyntax
708708
{
709709
declSyntax = DeclSyntax(visitBodyAndPreambleMacros(declNodeWithBody))
710+
} else if let varDecl = node.as(VariableDeclSyntax.self),
711+
varDecl.bindings.count == 1,
712+
let bindingIndex = varDecl.bindings.indices.first,
713+
let accessorBlock = varDecl.bindings[bindingIndex].accessorBlock,
714+
case .getter = accessorBlock.accessors
715+
{
716+
let expandedBodies = expandMacros(
717+
attachedTo: DeclSyntax(varDecl),
718+
ofType: BodyMacro.Type.self
719+
) { attributeNode, definition, _ in
720+
expandBodyMacro(
721+
definition: definition,
722+
attributeNode: attributeNode,
723+
attachedTo: node,
724+
in: contextGenerator(Syntax(node)),
725+
indentationWidth: indentationWidth
726+
).map { [$0] }
727+
}
728+
729+
if let newBody = expandedBodies.first {
730+
let newAccessorBlock = accessorBlock.with(\.accessors, .getter(newBody.statements))
731+
let newBinding = varDecl.bindings[bindingIndex].with(\.accessorBlock, newAccessorBlock)
732+
var newBindings = varDecl.bindings
733+
newBindings[bindingIndex] = newBinding
734+
declSyntax = DeclSyntax(varDecl.with(\.bindings, newBindings))
735+
}
710736
}
711737

712738
// Visit the node, disabling the `visitAny` handling.

Sources/SwiftSyntaxMacros/Syntax+LexicalContext.swift

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,24 @@ extension SyntaxProtocol {
5555
case is EnumCaseElementSyntax:
5656
return Syntax(self.detached)
5757

58-
// Pattern bindings have their accessors and initializer removed.
58+
// Pattern bindings have their accessors and initializer removed, and wrapped in the parent variable decl.
5959
case var patternBinding as PatternBindingSyntax:
60+
let varDecl = patternBinding.parent?.as(PatternBindingListSyntax.self)?.parent?.as(VariableDeclSyntax.self)?
61+
.detached
6062
patternBinding = patternBinding.detached
6163
patternBinding.accessorBlock = nil
6264
patternBinding.initializer = nil
63-
return Syntax(patternBinding)
65+
if var varDecl {
66+
varDecl.bindings = [patternBinding]
67+
return Syntax(varDecl)
68+
} else {
69+
return Syntax(
70+
VariableDeclSyntax(
71+
bindingSpecifier: .keyword(.var),
72+
bindings: [patternBinding]
73+
)
74+
)
75+
}
6476

6577
// Freestanding macros are fine as-is because if any arguments change
6678
// the whole macro would have to be re-evaluated.

Tests/SwiftSyntaxMacroExpansionTest/BodyMacroTests.swift

Lines changed: 99 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,32 @@ struct StartTaskMacro: BodyMacro {
7272
guard let taskBody = declaration.body else {
7373
return []
7474
}
75-
return [
76-
"""
77-
Task \(taskBody.trimmed)
78-
"""
79-
]
75+
76+
let taskName: String?
77+
if let funcDecl = declaration.as(FunctionDeclSyntax.self) {
78+
taskName = funcDecl.name.text
79+
} else if declaration.is(AccessorDeclSyntax.self) {
80+
taskName = context.lexicalContext
81+
.compactMap { $0.as(VariableDeclSyntax.self) }
82+
.first
83+
.flatMap { $0.bindings.first?.pattern.as(IdentifierPatternSyntax.self)?.identifier.text }
84+
} else {
85+
taskName = nil
86+
}
87+
88+
if let taskName {
89+
return [
90+
"""
91+
Task(name: \(literal: taskName)) \(taskBody.trimmed)
92+
"""
93+
]
94+
} else {
95+
return [
96+
"""
97+
Task \(taskBody.trimmed)
98+
"""
99+
]
100+
}
80101
}
81102

82103
static func expansion(
@@ -231,6 +252,79 @@ final class BodyMacroTests: XCTestCase {
231252
)
232253
}
233254

255+
func testBodyExpansionOnFunc() {
256+
assertMacroExpansion(
257+
"""
258+
@StartTask
259+
func x() -> Int {
260+
a + b
261+
}
262+
""",
263+
expandedSource: """
264+
265+
func x() -> Int {
266+
Task(name: "x") {
267+
a + b
268+
}
269+
}
270+
""",
271+
macros: ["StartTask": StartTaskMacro.self],
272+
indentationWidth: indentationWidth
273+
)
274+
}
275+
276+
func testBodyExpansionOnComputedVar() {
277+
assertMacroExpansion(
278+
"""
279+
@StartTask
280+
var x: Int {
281+
a + b
282+
}
283+
""",
284+
expandedSource: """
285+
286+
var x: Int {
287+
Task(name: "x") {
288+
a + b
289+
}
290+
}
291+
""",
292+
macros: ["StartTask": StartTaskMacro.self],
293+
indentationWidth: indentationWidth
294+
)
295+
}
296+
297+
func testBodyExpansionOnAccessors() {
298+
assertMacroExpansion(
299+
"""
300+
var value: Double {
301+
@StartTask get {
302+
return length * width
303+
}
304+
@StartTask set {
305+
self.value = newValue * 2
306+
}
307+
}
308+
""",
309+
expandedSource: """
310+
var value: Double {
311+
get {
312+
Task(name: "value") {
313+
return length * width
314+
}
315+
}
316+
set {
317+
Task(name: "value") {
318+
self.value = newValue * 2
319+
}
320+
}
321+
}
322+
""",
323+
macros: ["StartTask": StartTaskMacro.self],
324+
indentationWidth: indentationWidth
325+
)
326+
}
327+
234328
func testClosureBodyExpansion() {
235329
assertMacroExpansion(
236330
"""

Tests/SwiftSyntaxMacroExpansionTest/LexicalContextTests.swift

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,14 @@ extension SyntaxProtocol {
151151
return singleVarName
152152
}
153153

154+
// Variable declarations with a single binding.
155+
if let varDecl = self.as(VariableDeclSyntax.self),
156+
varDecl.bindings.count == 1,
157+
let singleVarName = varDecl.bindings.first?.singleBindingName
158+
{
159+
return singleVarName
160+
}
161+
154162
return nil
155163
}
156164
}
@@ -303,6 +311,21 @@ final class LexicalContextTests: XCTestCase {
303311
indentationWidth: indentationWidth
304312
)
305313

314+
assertMacroExpansion(
315+
"""
316+
extension A {
317+
static var staticProp = #function, secondProp = #function
318+
}
319+
""",
320+
expandedSource: """
321+
extension A {
322+
static var staticProp = "staticProp", secondProp = "secondProp"
323+
}
324+
""",
325+
macros: ["function": FunctionMacro.self],
326+
indentationWidth: indentationWidth
327+
)
328+
306329
assertMacroExpansion(
307330
"""
308331
func f(a: Int, _: Double, c: Int) {
@@ -546,7 +569,7 @@ final class LexicalContextTests: XCTestCase {
546569
await _
547570
try _
548571
unsafe _
549-
contextDescription: String
572+
var contextDescription: String
550573
struct S {}
551574
{ c in
552575
}

0 commit comments

Comments
 (0)