Skip to content

Commit 63bb090

Browse files
committed
Make the "are syntax errors allowed?" condition independent of evaluation
The compiler needs to be more careful about when it actually evaluates an #if condition, because canImport can have side effects. Make it easier to answer the question "are syntax errors allowed?" without having to evaluate the condition every time.
1 parent 19445c0 commit 63bb090

5 files changed

+91
-25
lines changed

Sources/SwiftIfConfig/ConfiguredRegions.swift

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,18 @@ fileprivate class ConfiguredRegionVisitor<Configuration: BuildConfiguration>: Sy
6565
// If we're in an active region, find the active clause. Otherwise,
6666
// there isn't one.
6767
let activeClause = inActiveRegion ? node.activeClause(in: configuration).clause : nil
68+
var foundActive = false
69+
var syntaxErrorsAllowed = false
6870
for clause in node.clauses {
71+
// If we haven't found the active clause yet, syntax errors are allowed
72+
// depending on this clause.
73+
if !foundActive {
74+
syntaxErrorsAllowed =
75+
clause.condition.map {
76+
IfConfigClauseSyntax.syntaxErrorsAllowed($0).syntaxErrorsAllowed
77+
} ?? false
78+
}
79+
6980
// If this is the active clause, record it and then recurse into the
7081
// elements.
7182
if clause == activeClause {
@@ -77,14 +88,10 @@ fileprivate class ConfiguredRegionVisitor<Configuration: BuildConfiguration>: Sy
7788
walk(elements)
7889
}
7990

91+
foundActive = true
8092
continue
8193
}
8294

83-
// For inactive clauses, distinguish between inactive and unparsed.
84-
let syntaxErrorsAllowed = clause.syntaxErrorsAllowed(
85-
configuration: configuration
86-
).syntaxErrorsAllowed
87-
8895
// If this is within an active region, or this is an unparsed region,
8996
// record it.
9097
if inActiveRegion || syntaxErrorsAllowed {

Sources/SwiftIfConfig/IfConfigDecl+IfConfig.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,13 @@ extension IfConfigDeclSyntax {
4343
return (clause, diagnostics: diagnostics)
4444
}
4545

46+
// Apply operator folding for !/&&/||.
47+
let (foldedCondition, foldingDiagnostics) = IfConfigClauseSyntax.foldOperators(condition)
48+
diagnostics.append(contentsOf: foldingDiagnostics)
49+
4650
// If this condition evaluates true, return this clause.
4751
let (isActive, _, localDiagnostics) = evaluateIfConfig(
48-
condition: condition,
52+
condition: foldedCondition,
4953
configuration: configuration
5054
)
5155
diagnostics.append(contentsOf: localDiagnostics)

Sources/SwiftIfConfig/IfConfigEvaluation.swift

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import SwiftDiagnostics
14+
import SwiftOperators
1415
import SwiftSyntax
1516

1617
/// Evaluate the condition of an `#if`.
@@ -433,20 +434,77 @@ func evaluateIfConfig(
433434
}
434435

435436
extension IfConfigClauseSyntax {
436-
/// Determine whether this condition is "syntaxErrorsAllowed".
437-
func syntaxErrorsAllowed(
438-
configuration: some BuildConfiguration
437+
/// Fold the operators within an #if condition, turning sequence expressions
438+
/// involving the various allowed operators (&&, ||, !) into well-structured
439+
/// binary operators.
440+
public static func foldOperators(
441+
_ condition: some ExprSyntaxProtocol
442+
) -> (folded: ExprSyntax, diagnostics: [Diagnostic]) {
443+
var foldingDiagnostics: [Diagnostic] = []
444+
let foldedCondition = OperatorTable.logicalOperators.foldAll(condition) { error in
445+
foldingDiagnostics.append(contentsOf: error.asDiagnostics(at: condition))
446+
}.cast(ExprSyntax.self)
447+
return (folded: foldedCondition, diagnostics: foldingDiagnostics)
448+
}
449+
450+
/// Determine whether the given expression, when used as the condition in
451+
/// an inactive `#if` clause, implies that syntax errors are permitted within
452+
/// that region.
453+
public static func syntaxErrorsAllowed(
454+
_ condition: some ExprSyntaxProtocol
439455
) -> (syntaxErrorsAllowed: Bool, diagnostics: [Diagnostic]) {
440-
guard let condition else {
441-
return (syntaxErrorsAllowed: false, diagnostics: [])
442-
}
456+
let (foldedCondition, foldingDiagnostics) = IfConfigClauseSyntax.foldOperators(condition)
443457

444-
// Evaluate this condition against the build configuration.
445-
let (_, syntaxErrorsAllowed, diagnostics) = evaluateIfConfig(
446-
condition: condition,
447-
configuration: configuration
458+
return (
459+
!foldingDiagnostics.isEmpty || foldedCondition.allowsSyntaxErrorsFolded,
460+
foldingDiagnostics
448461
)
462+
}
463+
}
464+
465+
extension ExprSyntaxProtocol {
466+
/// Determine whether this expression, when used as a condition within a #if
467+
/// that evaluates false, implies that the code contained in that `#if`
468+
///
469+
/// Check whether of allowsSyntaxErrors(_:) that assumes that inputs have
470+
/// already been operator-folded.
471+
var allowsSyntaxErrorsFolded: Bool {
472+
// Logical '!'.
473+
if let prefixOp = self.as(PrefixOperatorExprSyntax.self),
474+
prefixOp.operator.text == "!"
475+
{
476+
return prefixOp.expression.allowsSyntaxErrorsFolded
477+
}
478+
479+
// Logical '&&' and '||'.
480+
if let binOp = self.as(InfixOperatorExprSyntax.self),
481+
let op = binOp.operator.as(BinaryOperatorExprSyntax.self)
482+
{
483+
switch op.operator.text {
484+
case "&&":
485+
return binOp.leftOperand.allowsSyntaxErrorsFolded || binOp.rightOperand.allowsSyntaxErrorsFolded
486+
case "||":
487+
return binOp.leftOperand.allowsSyntaxErrorsFolded && binOp.rightOperand.allowsSyntaxErrorsFolded
488+
default:
489+
return false
490+
}
491+
}
492+
493+
// Look through parentheses.
494+
if let tuple = self.as(TupleExprSyntax.self), tuple.isParentheses,
495+
let element = tuple.elements.first
496+
{
497+
return element.expression.allowsSyntaxErrorsFolded
498+
}
499+
500+
// Call syntax is for operations.
501+
if let call = self.as(FunctionCallExprSyntax.self),
502+
let fnName = call.calledExpression.simpleIdentifierExpr,
503+
let fn = IfConfigFunctions(rawValue: fnName)
504+
{
505+
return fn.syntaxErrorsAllowed
506+
}
449507

450-
return (syntaxErrorsAllowed, diagnostics)
508+
return false
451509
}
452510
}

Sources/SwiftIfConfig/IfConfigRegionState.swift

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,7 @@ public enum IfConfigRegionState {
3232
in configuration: some BuildConfiguration
3333
) -> (state: IfConfigRegionState, syntaxErrorsAllowed: Bool, diagnostics: [Diagnostic]) {
3434
// Apply operator folding for !/&&/||.
35-
var foldingDiagnostics: [Diagnostic] = []
36-
let foldedCondition = OperatorTable.logicalOperators.foldAll(condition) { error in
37-
foldingDiagnostics.append(contentsOf: error.asDiagnostics(at: condition))
38-
}.cast(ExprSyntax.self)
35+
let (foldedCondition, foldingDiagnostics) = IfConfigClauseSyntax.foldOperators(condition)
3936

4037
let (active, syntaxErrorsAllowed, evalDiagnostics) = evaluateIfConfig(
4138
condition: foldedCondition,

Sources/SwiftIfConfig/SyntaxProtocol+IfConfig.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@ extension SyntaxProtocol {
5757
// This was not the active clause, so we know that we're in an
5858
// inactive block. If syntax errors aren't allowable, this is an
5959
// unparsed region.
60-
let (syntaxErrorsAllowed, localDiagnostics) = ifConfigClause.syntaxErrorsAllowed(
61-
configuration: configuration
62-
)
63-
diagnostics.append(contentsOf: localDiagnostics)
60+
let syntaxErrorsAllowed =
61+
ifConfigClause.condition.map {
62+
IfConfigClauseSyntax.syntaxErrorsAllowed($0).syntaxErrorsAllowed
63+
} ?? false
6464

6565
if syntaxErrorsAllowed {
6666
return (.unparsed, diagnostics)

0 commit comments

Comments
 (0)