diff --git a/Sources/SwiftIfConfig/ConfiguredRegions.swift b/Sources/SwiftIfConfig/ConfiguredRegions.swift index f562e770bd5..f4553e09a7b 100644 --- a/Sources/SwiftIfConfig/ConfiguredRegions.swift +++ b/Sources/SwiftIfConfig/ConfiguredRegions.swift @@ -65,7 +65,18 @@ fileprivate class ConfiguredRegionVisitor: Sy // If we're in an active region, find the active clause. Otherwise, // there isn't one. let activeClause = inActiveRegion ? node.activeClause(in: configuration).clause : nil + var foundActive = false + var syntaxErrorsAllowed = false for clause in node.clauses { + // If we haven't found the active clause yet, syntax errors are allowed + // depending on this clause. + if !foundActive { + syntaxErrorsAllowed = + clause.condition.map { + IfConfigClauseSyntax.syntaxErrorsAllowed($0).syntaxErrorsAllowed + } ?? false + } + // If this is the active clause, record it and then recurse into the // elements. if clause == activeClause { @@ -77,14 +88,10 @@ fileprivate class ConfiguredRegionVisitor: Sy walk(elements) } + foundActive = true continue } - // For inactive clauses, distinguish between inactive and unparsed. - let syntaxErrorsAllowed = clause.syntaxErrorsAllowed( - configuration: configuration - ).syntaxErrorsAllowed - // If this is within an active region, or this is an unparsed region, // record it. if inActiveRegion || syntaxErrorsAllowed { diff --git a/Sources/SwiftIfConfig/IfConfigDecl+IfConfig.swift b/Sources/SwiftIfConfig/IfConfigDecl+IfConfig.swift index 68e9e6b7b48..d5d286d4ba7 100644 --- a/Sources/SwiftIfConfig/IfConfigDecl+IfConfig.swift +++ b/Sources/SwiftIfConfig/IfConfigDecl+IfConfig.swift @@ -43,9 +43,13 @@ extension IfConfigDeclSyntax { return (clause, diagnostics: diagnostics) } + // Apply operator folding for !/&&/||. + let (foldedCondition, foldingDiagnostics) = IfConfigClauseSyntax.foldOperators(condition) + diagnostics.append(contentsOf: foldingDiagnostics) + // If this condition evaluates true, return this clause. let (isActive, _, localDiagnostics) = evaluateIfConfig( - condition: condition, + condition: foldedCondition, configuration: configuration ) diagnostics.append(contentsOf: localDiagnostics) diff --git a/Sources/SwiftIfConfig/IfConfigEvaluation.swift b/Sources/SwiftIfConfig/IfConfigEvaluation.swift index ac4b95e350d..2ed3ccb4c37 100644 --- a/Sources/SwiftIfConfig/IfConfigEvaluation.swift +++ b/Sources/SwiftIfConfig/IfConfigEvaluation.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// import SwiftDiagnostics +import SwiftOperators import SwiftSyntax /// Evaluate the condition of an `#if`. @@ -433,20 +434,77 @@ func evaluateIfConfig( } extension IfConfigClauseSyntax { - /// Determine whether this condition is "syntaxErrorsAllowed". - func syntaxErrorsAllowed( - configuration: some BuildConfiguration + /// Fold the operators within an #if condition, turning sequence expressions + /// involving the various allowed operators (&&, ||, !) into well-structured + /// binary operators. + public static func foldOperators( + _ condition: some ExprSyntaxProtocol + ) -> (folded: ExprSyntax, diagnostics: [Diagnostic]) { + var foldingDiagnostics: [Diagnostic] = [] + let foldedCondition = OperatorTable.logicalOperators.foldAll(condition) { error in + foldingDiagnostics.append(contentsOf: error.asDiagnostics(at: condition)) + }.cast(ExprSyntax.self) + return (folded: foldedCondition, diagnostics: foldingDiagnostics) + } + + /// Determine whether the given expression, when used as the condition in + /// an inactive `#if` clause, implies that syntax errors are permitted within + /// that region. + public static func syntaxErrorsAllowed( + _ condition: some ExprSyntaxProtocol ) -> (syntaxErrorsAllowed: Bool, diagnostics: [Diagnostic]) { - guard let condition else { - return (syntaxErrorsAllowed: false, diagnostics: []) - } + let (foldedCondition, foldingDiagnostics) = IfConfigClauseSyntax.foldOperators(condition) - // Evaluate this condition against the build configuration. - let (_, syntaxErrorsAllowed, diagnostics) = evaluateIfConfig( - condition: condition, - configuration: configuration + return ( + !foldingDiagnostics.isEmpty || foldedCondition.allowsSyntaxErrorsFolded, + foldingDiagnostics ) + } +} + +extension ExprSyntaxProtocol { + /// Determine whether this expression, when used as a condition within a #if + /// that evaluates false, implies that the code contained in that `#if` + /// + /// Check whether of allowsSyntaxErrors(_:) that assumes that inputs have + /// already been operator-folded. + var allowsSyntaxErrorsFolded: Bool { + // Logical '!'. + if let prefixOp = self.as(PrefixOperatorExprSyntax.self), + prefixOp.operator.text == "!" + { + return prefixOp.expression.allowsSyntaxErrorsFolded + } + + // Logical '&&' and '||'. + if let binOp = self.as(InfixOperatorExprSyntax.self), + let op = binOp.operator.as(BinaryOperatorExprSyntax.self) + { + switch op.operator.text { + case "&&": + return binOp.leftOperand.allowsSyntaxErrorsFolded || binOp.rightOperand.allowsSyntaxErrorsFolded + case "||": + return binOp.leftOperand.allowsSyntaxErrorsFolded && binOp.rightOperand.allowsSyntaxErrorsFolded + default: + return false + } + } + + // Look through parentheses. + if let tuple = self.as(TupleExprSyntax.self), tuple.isParentheses, + let element = tuple.elements.first + { + return element.expression.allowsSyntaxErrorsFolded + } + + // Call syntax is for operations. + if let call = self.as(FunctionCallExprSyntax.self), + let fnName = call.calledExpression.simpleIdentifierExpr, + let fn = IfConfigFunctions(rawValue: fnName) + { + return fn.syntaxErrorsAllowed + } - return (syntaxErrorsAllowed, diagnostics) + return false } } diff --git a/Sources/SwiftIfConfig/IfConfigRegionState.swift b/Sources/SwiftIfConfig/IfConfigRegionState.swift index 50a1ab0369b..5d2553d1d4a 100644 --- a/Sources/SwiftIfConfig/IfConfigRegionState.swift +++ b/Sources/SwiftIfConfig/IfConfigRegionState.swift @@ -32,10 +32,7 @@ public enum IfConfigRegionState { in configuration: some BuildConfiguration ) -> (state: IfConfigRegionState, syntaxErrorsAllowed: Bool, diagnostics: [Diagnostic]) { // Apply operator folding for !/&&/||. - var foldingDiagnostics: [Diagnostic] = [] - let foldedCondition = OperatorTable.logicalOperators.foldAll(condition) { error in - foldingDiagnostics.append(contentsOf: error.asDiagnostics(at: condition)) - }.cast(ExprSyntax.self) + let (foldedCondition, foldingDiagnostics) = IfConfigClauseSyntax.foldOperators(condition) let (active, syntaxErrorsAllowed, evalDiagnostics) = evaluateIfConfig( condition: foldedCondition, diff --git a/Sources/SwiftIfConfig/SyntaxProtocol+IfConfig.swift b/Sources/SwiftIfConfig/SyntaxProtocol+IfConfig.swift index 7540d4a544e..4f87ad40877 100644 --- a/Sources/SwiftIfConfig/SyntaxProtocol+IfConfig.swift +++ b/Sources/SwiftIfConfig/SyntaxProtocol+IfConfig.swift @@ -57,10 +57,10 @@ extension SyntaxProtocol { // This was not the active clause, so we know that we're in an // inactive block. If syntax errors aren't allowable, this is an // unparsed region. - let (syntaxErrorsAllowed, localDiagnostics) = ifConfigClause.syntaxErrorsAllowed( - configuration: configuration - ) - diagnostics.append(contentsOf: localDiagnostics) + let syntaxErrorsAllowed = + ifConfigClause.condition.map { + IfConfigClauseSyntax.syntaxErrorsAllowed($0).syntaxErrorsAllowed + } ?? false if syntaxErrorsAllowed { return (.unparsed, diagnostics)