Skip to content

Commit 45bf2b8

Browse files
committed
Introduce the ability to retain compiler and feature checks when removing inactive regions
The compiler's handling of inlinable text for Swift interface checking requires the ability to retain `#if`s involving compiler checks (e.g., `#if compiler(>=6.0)`) and `$`-based feature checks (`#if $AsyncAwait`) when stripping inactive regions. Expand the `removingInactive` function with a parameter that implements this behavior.
1 parent 80f3a34 commit 45bf2b8

File tree

2 files changed

+130
-8
lines changed

2 files changed

+130
-8
lines changed

Sources/SwiftIfConfig/ActiveSyntaxRewriter.swift

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,20 @@ extension SyntaxProtocol {
1818
/// are inactive according to the given build configuration, leaving only
1919
/// the code that is active within that build configuration.
2020
///
21-
/// Returns the syntax node with all inactive regions removed, along with an
22-
/// array containing any diagnostics produced along the way.
23-
///
2421
/// If there are errors in the conditions of any configuration
2522
/// clauses, e.g., `#if FOO > 10`, then the condition will be
2623
/// considered to have failed and the clauses's elements will be
2724
/// removed.
25+
/// - Parameters:
26+
/// - configuration: the configuration to apply.
27+
/// - retainFeatureCheckIfConfigs: whether to retain `#if` blocks involving
28+
/// compiler version checks (e.g., `compiler(>=6.0)`) and `$`-based
29+
/// feature checks.
30+
/// - Returns: the syntax node with all inactive regions removed, along with
31+
/// an array containing any diagnostics produced along the way.
2832
public func removingInactive(
29-
in configuration: some BuildConfiguration
33+
in configuration: some BuildConfiguration,
34+
retainFeatureCheckIfConfigs: Bool = false
3035
) -> (result: Syntax, diagnostics: [Diagnostic]) {
3136
// First pass: Find all of the active clauses for the #ifs we need to
3237
// visit, along with any diagnostics produced along the way. This process
@@ -41,7 +46,10 @@ extension SyntaxProtocol {
4146

4247
// Second pass: Rewrite the syntax tree by removing the inactive clauses
4348
// from each #if (along with the #ifs themselves).
44-
let rewriter = ActiveSyntaxRewriter(configuration: configuration)
49+
let rewriter = ActiveSyntaxRewriter(
50+
configuration: configuration,
51+
retainFeatureCheckIfConfigs: retainFeatureCheckIfConfigs
52+
)
4553
return (
4654
rewriter.rewrite(Syntax(self)),
4755
visitor.diagnostics
@@ -83,8 +91,12 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
8391
let configuration: Configuration
8492
var diagnostics: [Diagnostic] = []
8593

86-
init(configuration: Configuration) {
94+
/// Whether to retain `#if` blocks containing compiler and feature checks.
95+
var retainFeatureCheckIfConfigs: Bool
96+
97+
init(configuration: Configuration, retainFeatureCheckIfConfigs: Bool) {
8798
self.configuration = configuration
99+
self.retainFeatureCheckIfConfigs = retainFeatureCheckIfConfigs
88100
}
89101

90102
private func dropInactive<List: SyntaxCollection>(
@@ -97,7 +109,9 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
97109
let element = node[elementIndex]
98110

99111
// Find #ifs within the list.
100-
if let ifConfigDecl = elementAsIfConfig(element) {
112+
if let ifConfigDecl = elementAsIfConfig(element),
113+
(!retainFeatureCheckIfConfigs || !ifConfigDecl.containsFeatureCheck)
114+
{
101115
// Retrieve the active `#if` clause
102116
let (activeClause, localDiagnostics) = ifConfigDecl.activeClause(in: configuration)
103117

@@ -262,6 +276,12 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
262276
outerBase: ExprSyntax?,
263277
postfixIfConfig: PostfixIfConfigExprSyntax
264278
) -> ExprSyntax {
279+
// If we're supposed to retain #if configs that are feature checks, and
280+
// this configuration has one, do so.
281+
if retainFeatureCheckIfConfigs && postfixIfConfig.config.containsFeatureCheck {
282+
return ExprSyntax(postfixIfConfig)
283+
}
284+
265285
// Retrieve the active `if` clause.
266286
let (activeClause, localDiagnostics) = postfixIfConfig.config.activeClause(in: configuration)
267287

@@ -307,3 +327,55 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
307327
return visit(rewrittenNode)
308328
}
309329
}
330+
331+
/// Helper class to find a feature or compiler check.
332+
fileprivate class FindFeatureCheckVisitor: SyntaxVisitor {
333+
var foundFeatureCheck = false
334+
335+
override func visit(_ node: DeclReferenceExprSyntax) -> SyntaxVisitorContinueKind {
336+
// Checks that start with $ are feature checks that should be retained.
337+
if let identifier = node.simpleIdentifier,
338+
let initialChar = identifier.name.first,
339+
initialChar == "$"
340+
{
341+
foundFeatureCheck = true
342+
return .skipChildren
343+
}
344+
345+
return .visitChildren
346+
}
347+
348+
override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind {
349+
if let calleeDeclRef = node.calledExpression.as(DeclReferenceExprSyntax.self),
350+
let calleeName = calleeDeclRef.simpleIdentifier?.name,
351+
(calleeName == "compiler" || calleeName == "_compiler_version")
352+
{
353+
foundFeatureCheck = true
354+
}
355+
356+
return .skipChildren
357+
}
358+
}
359+
360+
extension ExprSyntaxProtocol {
361+
/// Whether any of the nodes in this expression involve compiler or feature
362+
/// checks.
363+
fileprivate var containsFeatureCheck: Bool {
364+
let visitor = FindFeatureCheckVisitor(viewMode: .fixedUp)
365+
visitor.walk(self)
366+
return visitor.foundFeatureCheck
367+
}
368+
}
369+
370+
extension IfConfigDeclSyntax {
371+
/// Whether any of the clauses in this #if contain a feature check.
372+
var containsFeatureCheck: Bool {
373+
return clauses.contains { clause in
374+
if let condition = clause.condition {
375+
return condition.containsFeatureCheck
376+
} else {
377+
return false
378+
}
379+
}
380+
}
381+
}

Tests/SwiftIfConfigTest/VisitorTests.swift

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,13 +253,60 @@ public class VisitorTests: XCTestCase {
253253
"""
254254
)
255255
}
256+
257+
func testRemoveInactiveRetainingFeatureChecks() {
258+
assertRemoveInactive(
259+
"""
260+
public func hasIfCompilerCheck(_ x: () -> Bool = {
261+
#if compiler(>=5.3)
262+
return true
263+
#else
264+
return false
265+
#endif
266+
267+
#if $Blah
268+
return 0
269+
#else
270+
return 1
271+
#endif
272+
273+
#if NOT_SET
274+
return 3.14159
275+
#else
276+
return 2.71828
277+
#endif
278+
}) {
279+
}
280+
""",
281+
configuration: linuxBuildConfig,
282+
retainFeatureCheckIfConfigs: true,
283+
expectedSource: """
284+
public func hasIfCompilerCheck(_ x: () -> Bool = {
285+
#if compiler(>=5.3)
286+
return true
287+
#else
288+
return false
289+
#endif
290+
291+
#if $Blah
292+
return 0
293+
#else
294+
return 1
295+
#endif
296+
return 2.71828
297+
}) {
298+
}
299+
"""
300+
)
301+
}
256302
}
257303

258304
/// Assert that applying the given build configuration to the source code
259305
/// returns the expected source and diagnostics.
260306
fileprivate func assertRemoveInactive(
261307
_ source: String,
262308
configuration: some BuildConfiguration,
309+
retainFeatureCheckIfConfigs: Bool = false,
263310
diagnostics expectedDiagnostics: [DiagnosticSpec] = [],
264311
expectedSource: String,
265312
file: StaticString = #filePath,
@@ -268,7 +315,10 @@ fileprivate func assertRemoveInactive(
268315
var parser = Parser(source)
269316
let tree = SourceFileSyntax.parse(from: &parser)
270317

271-
let (treeWithoutInactive, actualDiagnostics) = tree.removingInactive(in: configuration)
318+
let (treeWithoutInactive, actualDiagnostics) = tree.removingInactive(
319+
in: configuration,
320+
retainFeatureCheckIfConfigs: retainFeatureCheckIfConfigs
321+
)
272322

273323
// Check the resulting tree.
274324
assertStringsEqualWithDiff(

0 commit comments

Comments
 (0)