Skip to content

Commit 2d2021f

Browse files
committed
Implement inactive clause rewriting support for postfix #if
Postfix `#if` expressions have a different syntactic form than other `#if` clauses because they don't fit into a list-like position in the grammar. Implement a separate, recursive folding algorithm to handle these clauses.
1 parent 623ffc7 commit 2d2021f

File tree

2 files changed

+137
-2
lines changed

2 files changed

+137
-2
lines changed

Sources/SwiftIfConfig/IfConfigRewriter.swift

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,122 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration> : SyntaxRewriter {
146146

147147
return super.visit(rewrittenNode)
148148
}
149+
150+
/// Apply the given base to the postfix expression.
151+
private func applyBaseToPostfixExpression(
152+
base: ExprSyntax, postfix: ExprSyntax
153+
) -> ExprSyntax {
154+
/// Try to apply the base to the postfix expression using the given
155+
/// keypath into a specific node type.
156+
///
157+
/// Returns the new expression node on success, `nil` when the node kind
158+
/// didn't match.
159+
func tryApply<Node: ExprSyntaxProtocol>(
160+
_ keyPath: WritableKeyPath<Node, ExprSyntax>
161+
) -> ExprSyntax? {
162+
guard let node = postfix.as(Node.self) else {
163+
return nil
164+
}
165+
166+
let newExpr = applyBaseToPostfixExpression(base: base, postfix: node[keyPath: keyPath])
167+
return ExprSyntax(node.with(keyPath, newExpr))
168+
}
169+
170+
// Member access
171+
if let memberAccess = postfix.as(MemberAccessExprSyntax.self) {
172+
guard let memberBase = memberAccess.base else {
173+
// If this member access has no base, this is the base we are
174+
// replacing, terminating the recursion. Do so now.
175+
return ExprSyntax(memberAccess.with(\.base, base))
176+
}
177+
178+
let newBase = applyBaseToPostfixExpression(base: base, postfix: memberBase)
179+
return ExprSyntax(memberAccess.with(\.base, newBase))
180+
}
181+
182+
// Generic arguments <...>
183+
if let result = tryApply(\SpecializeExprSyntax.expression) {
184+
return result
185+
}
186+
187+
// Call (...)
188+
if let result = tryApply(\FunctionCallExprSyntax.calledExpression) {
189+
return result
190+
}
191+
192+
// Subscript [...]
193+
if let result = tryApply(\SubscriptExprSyntax.calledExpression) {
194+
return result
195+
}
196+
197+
// Optional chaining ?
198+
if let result = tryApply(\OptionalChainingExprSyntax.expression) {
199+
return result
200+
}
201+
202+
// Forced optional value !
203+
if let result = tryApply(\ForcedValueExprSyntax.expression) {
204+
return result
205+
}
206+
207+
// Postfix unary operator.
208+
if let result = tryApply(\PostfixUnaryExprSyntax.expression) {
209+
return result
210+
}
211+
212+
// #if
213+
if let postfixIfConfig = postfix.as(PostfixIfConfigExprSyntax.self) {
214+
return dropInactive(outerBase: base, postfixIfConfig: postfixIfConfig)
215+
}
216+
217+
assert(false, "Unhandled postfix expression in #if elimination")
218+
return base
219+
}
220+
221+
/// Drop inactive regions from a postfix `#if` configuration, applying the
222+
/// outer "base" expression to the rewritten node.
223+
private func dropInactive(
224+
outerBase: ExprSyntax?,
225+
postfixIfConfig: PostfixIfConfigExprSyntax
226+
) -> ExprSyntax {
227+
// Determine the active clause within this syntax node.
228+
// TODO: Swallows errors
229+
guard let activeClause = try? postfixIfConfig.config.activeClause(in: configuration),
230+
case .`postfixExpression`(let postfixExpr) = activeClause.elements else {
231+
// If there is no active clause, return the base.
232+
233+
// Prefer the base we have and, if not, use the outer base.
234+
// TODO: Can we have both? If so, then what?
235+
if let base = postfixIfConfig.base ?? outerBase {
236+
return base
237+
}
238+
239+
// If there was no base, we're in an erroneous syntax tree that would
240+
// never be produced by the parser. Synthesize a missing expression
241+
// syntax node so clients can recover more gracefully.
242+
return ExprSyntax(
243+
MissingExprSyntax(
244+
placeholder: .init(.identifier("_"), presence: .missing)
245+
)
246+
)
247+
}
248+
249+
// If there is no base, return the postfix expression.
250+
guard let base = postfixIfConfig.base ?? outerBase else {
251+
return postfixExpr
252+
}
253+
254+
// Apply the base to the postfix expression.
255+
return applyBaseToPostfixExpression(base: base, postfix: postfixExpr)
256+
}
257+
258+
// TODO: PostfixIfConfigExprSyntax has a different form that doesn't work
259+
// well with the way dropInactive is written. We essentially need to
260+
// thread a the "base" into the active clause.
261+
override func visit(_ node: PostfixIfConfigExprSyntax) -> ExprSyntax {
262+
let rewrittenNode = dropInactive(outerBase: nil, postfixIfConfig: node)
263+
return visit(rewrittenNode)
264+
}
149265
}
150266

151267

Tests/SwiftIfConfigTest/VisitorTests.swift

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,19 @@ public class VisitorTests: XCTestCase {
7979
#endif
8080
}
8181
}
82+
83+
func i() {
84+
a.b
85+
#if DEBUG
86+
.c
87+
#endif
88+
#if hasAttribute(available)
89+
.d()
90+
#endif
91+
#if os(iOS)
92+
.e[]
93+
#endif
94+
}
8295
#endif
8396
"""
8497

@@ -130,12 +143,12 @@ public class VisitorTests: XCTestCase {
130143
// Check that the right set of names is visited.
131144
NameCheckingVisitor(
132145
configuration: linuxBuildConfig,
133-
expectedNames: ["f", "h", "S", "generationCount", "value"]
146+
expectedNames: ["f", "h", "i", "S", "generationCount", "value"]
134147
).walk(inputSource)
135148

136149
NameCheckingVisitor(
137150
configuration: iosBuildConfig,
138-
expectedNames: ["g", "h", "a", "S", "generationCount", "value", "error"]
151+
expectedNames: ["g", "h", "i", "a", "S", "generationCount", "value", "error"]
139152
).walk(inputSource)
140153
}
141154

@@ -158,6 +171,12 @@ public class VisitorTests: XCTestCase {
158171
break
159172
}
160173
}
174+
175+
func i() {
176+
a.b
177+
.c
178+
.d()
179+
}
161180
""")
162181
}
163182
}

0 commit comments

Comments
 (0)