Skip to content

Commit

Permalink
Reshuffle and rename some source files in SwiftIfConfig
Browse files Browse the repository at this point in the history
Rename source files in SwiftIfConfig to better reflect what they do, move
the public APIs up to the tops of files, and split the massive
IfConfigEvaluation.swift into several files. The file itself defines the
core logic for doing the evaluation (which is internal to the library),
and other source files provide public APIs on top of it.
  • Loading branch information
DougGregor committed Jul 7, 2024
1 parent bccddb4 commit aa97fbd
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 194 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,40 @@
import SwiftDiagnostics
import SwiftSyntax

extension SyntaxProtocol {
/// Produce a copy of this syntax node that removes all syntax regions that
/// are inactive according to the given build configuration, leaving only
/// the code that is active within that build configuration.
///
/// Returns the syntax node with all inactive regions removed, along with an
/// array containing any diagnostics produced along the way.
///
/// If there are errors in the conditions of any configuration
/// clauses, e.g., `#if FOO > 10`, then the condition will be
/// considered to have failed and the clauses's elements will be
/// removed.
public func removingInactive(in configuration: some BuildConfiguration) -> (Syntax, [Diagnostic]) {
// First pass: Find all of the active clauses for the #ifs we need to
// visit, along with any diagnostics produced along the way. This process
// does not change the tree in any way.
let visitor = ActiveSyntaxVisitor(viewMode: .sourceAccurate, configuration: configuration)
visitor.walk(self)

// If there were no active clauses to visit, we're done!
if visitor.numIfClausesVisited == 0 {
return (Syntax(self), visitor.diagnostics)
}

// Second pass: Rewrite the syntax tree by removing the inactive clauses
// from each #if (along with the #ifs themselves).
let rewriter = ActiveSyntaxRewriter(configuration: configuration)
return (
rewriter.rewrite(Syntax(self)),
visitor.diagnostics
)
}
}

/// Syntax rewriter that only visits syntax nodes that are active according
/// to a particular build configuration.
///
Expand Down Expand Up @@ -272,37 +306,3 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
return visit(rewrittenNode)
}
}

extension SyntaxProtocol {
/// Produce a copy of this syntax node that removes all syntax regions that
/// are inactive according to the given build configuration, leaving only
/// the code that is active within that build configuration.
///
/// Returns the syntax node with all inactive regions removed, along with an
/// array containing any diagnostics produced along the way.
///
/// If there are errors in the conditions of any configuration
/// clauses, e.g., `#if FOO > 10`, then the condition will be
/// considered to have failed and the clauses's elements will be
/// removed.
public func removingInactive(in configuration: some BuildConfiguration) -> (Syntax, [Diagnostic]) {
// First pass: Find all of the active clauses for the #ifs we need to
// visit, along with any diagnostics produced along the way. This process
// does not change the tree in any way.
let visitor = ActiveSyntaxVisitor(viewMode: .sourceAccurate, configuration: configuration)
visitor.walk(self)

// If there were no active clauses to visit, we're done!
if visitor.numIfClausesVisited == 0 {
return (Syntax(self), visitor.diagnostics)
}

// Second pass: Rewrite the syntax tree by removing the inactive clauses
// from each #if (along with the #ifs themselves).
let rewriter = ActiveSyntaxRewriter(configuration: configuration)
return (
rewriter.rewrite(Syntax(self)),
visitor.diagnostics
)
}
}
File renamed without changes.
6 changes: 4 additions & 2 deletions Sources/SwiftIfConfig/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@
# See http://swift.org/CONTRIBUTORS.txt for Swift project authors

add_swift_syntax_library(SwiftIfConfig
ActiveSyntaxVisitor.swift
ActiveSyntaxRewriter.swift
BuildConfiguration.swift
ConfiguredRegions.swift
ConfiguredRegionState.swift
IfConfigDecl+IfConfig.swift
IfConfigError.swift
IfConfigEvaluation.swift
IfConfigFunctions.swift
IfConfigRewriter.swift
IfConfigVisitor.swift
SyntaxLiteralUtils.swift
SyntaxProtocol+IfConfig.swift
VersionTuple+Parsing.swift
VersionTuple.swift
)
Expand Down
29 changes: 29 additions & 0 deletions Sources/SwiftIfConfig/ConfiguredRegionState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
import SwiftDiagnostics
import SwiftOperators
import SwiftSyntax

/// Describes the state of a particular region guarded by `#if` or similar.
public enum ConfiguredRegionState {
Expand All @@ -19,4 +22,30 @@ public enum ConfiguredRegionState {
case inactive
/// The region is active and is part of the compiled program.
case active

/// Evaluate the given `#if` condition using the given build configuration, throwing an error if there is
/// insufficient information to make a determination.
public init(
condition: some ExprSyntaxProtocol,
configuration: some BuildConfiguration,
diagnosticHandler: ((Diagnostic) -> Void)? = nil
) throws {
// Apply operator folding for !/&&/||.
let foldedCondition = try OperatorTable.logicalOperators.foldAll(condition) { error in
diagnosticHandler?(error.asDiagnostic)
throw error
}.cast(ExprSyntax.self)

let (active, versioned) = try evaluateIfConfig(
condition: foldedCondition,
configuration: configuration,
diagnosticHandler: diagnosticHandler
)

switch (active, versioned) {
case (true, _): self = .active
case (false, false): self = .inactive
case (false, true): self = .unparsed
}
}
}
1 change: 1 addition & 0 deletions Sources/SwiftIfConfig/ConfiguredRegions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ extension SyntaxProtocol {
}
}

/// Helper class that walks a syntax tree looking for configured regions.
fileprivate class ConfiguredRegionVisitor<Configuration: BuildConfiguration>: SyntaxVisitor {
let configuration: Configuration

Expand Down
59 changes: 59 additions & 0 deletions Sources/SwiftIfConfig/IfConfigDecl+IfConfig.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
import SwiftDiagnostics
import SwiftSyntax

extension IfConfigDeclSyntax {
/// Given a particular build configuration, determine which clause (if any) is the "active" clause.
///
/// For example, for code like the following:
/// ```
/// #if A
/// func f()
/// #elseif B
/// func g()
/// #endif
/// ```
///
/// If the `A` configuration option was passed on the command line (e.g. via `-DA`), the first clause
/// (containing `func f()`) would be returned. If not, and if the `B`configuration was passed on the
/// command line, the second clause (containing `func g()`) would be returned. If neither was
/// passed, this function will return `nil` to indicate that none of the regions are active.
///
/// If an error occurrs while processing any of the `#if` clauses,
/// that clause will be considered inactive and this operation will
/// continue to evaluate later clauses.
public func activeClause(
in configuration: some BuildConfiguration,
diagnosticHandler: ((Diagnostic) -> Void)? = nil
) -> IfConfigClauseSyntax? {
for clause in clauses {
// If there is no condition, we have reached an unconditional clause. Return it.
guard let condition = clause.condition else {
return clause
}

// If this condition evaluates true, return this clause.
let isActive =
(try? evaluateIfConfig(
condition: condition,
configuration: configuration,
diagnosticHandler: diagnosticHandler
))?.active ?? false
if isActive {
return clause
}
}

return nil
}
}
159 changes: 1 addition & 158 deletions Sources/SwiftIfConfig/IfConfigEvaluation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
//
//===----------------------------------------------------------------------===//
import SwiftDiagnostics
import SwiftOperators
import SwiftSyntax

/// Evaluate the condition of an `#if`.
Expand All @@ -29,7 +28,7 @@ import SwiftSyntax
/// condition holds with the given build configuration. The second whether
/// the build condition is a "versioned" check that implies that we shouldn't
/// diagnose syntax errors in blocks where the check fails.
private func evaluateIfConfig(
func evaluateIfConfig(
condition: ExprSyntax,
configuration: some BuildConfiguration,
diagnosticHandler: ((Diagnostic) -> Void)?
Expand Down Expand Up @@ -406,162 +405,6 @@ private func evaluateIfConfig(
throw recordedError(.unknownExpression(condition))
}

extension ConfiguredRegionState {
/// Evaluate the given `#if` condition using the given build configuration, throwing an error if there is
/// insufficient information to make a determination.
public init(
condition: some ExprSyntaxProtocol,
configuration: some BuildConfiguration,
diagnosticHandler: ((Diagnostic) -> Void)? = nil
) throws {
// Apply operator folding for !/&&/||.
let foldedCondition = try OperatorTable.logicalOperators.foldAll(condition) { error in
diagnosticHandler?(error.asDiagnostic)
throw error
}.cast(ExprSyntax.self)

let (active, versioned) = try evaluateIfConfig(
condition: foldedCondition,
configuration: configuration,
diagnosticHandler: diagnosticHandler
)

switch (active, versioned) {
case (true, _): self = .active
case (false, false): self = .inactive
case (false, true): self = .unparsed
}
}
}

extension IfConfigDeclSyntax {
/// Given a particular build configuration, determine which clause (if any) is the "active" clause.
///
/// For example, for code like the following:
/// ```
/// #if A
/// func f()
/// #elseif B
/// func g()
/// #endif
/// ```
///
/// If the `A` configuration option was passed on the command line (e.g. via `-DA`), the first clause
/// (containing `func f()`) would be returned. If not, and if the `B`configuration was passed on the
/// command line, the second clause (containing `func g()`) would be returned. If neither was
/// passed, this function will return `nil` to indicate that none of the regions are active.
///
/// If an error occurrs while processing any of the `#if` clauses,
/// that clause will be considered inactive and this operation will
/// continue to evaluate later clauses.
public func activeClause(
in configuration: some BuildConfiguration,
diagnosticHandler: ((Diagnostic) -> Void)? = nil
) -> IfConfigClauseSyntax? {
for clause in clauses {
// If there is no condition, we have reached an unconditional clause. Return it.
guard let condition = clause.condition else {
return clause
}

// If this condition evaluates true, return this clause.
let isActive =
(try? evaluateIfConfig(
condition: condition,
configuration: configuration,
diagnosticHandler: diagnosticHandler
))?.active ?? false
if isActive {
return clause
}
}

return nil
}
}

extension SyntaxProtocol {
/// Determine whether the given syntax node is active within the given build configuration.
///
/// This function evaluates the enclosing stack of `#if` conditions to determine whether the
/// given node is active in the program when it is compiled with the given build configuration.
///
/// For example, given code like the following:
/// #if DEBUG
/// #if A
/// func f()
/// #elseif B
/// func g()
/// #endif
/// #endif
///
/// a call to `isActive` on the syntax node for the function `g` would return `active` when the
/// configuration options `DEBUG` and `B` are provided, but `A` is not.
public func isActive(
in configuration: some BuildConfiguration,
diagnosticHandler: ((Diagnostic) -> Void)? = nil
) throws -> ConfiguredRegionState {
var currentNode: Syntax = Syntax(self)
var currentState: ConfiguredRegionState = .active

while let parent = currentNode.parent {
// If the parent is an `#if` configuration, check whether our current
// clause is active. If not, we're in an inactive region. We also
// need to determine whether
if let ifConfigClause = currentNode.as(IfConfigClauseSyntax.self),
let ifConfigDecl = ifConfigClause.parent?.parent?.as(IfConfigDeclSyntax.self)
{
let activeClause = ifConfigDecl.activeClause(
in: configuration,
diagnosticHandler: diagnosticHandler
)

if activeClause != ifConfigClause {
// This was not the active clause, so we know that we're in an
// inactive block. However, if the condition is versioned, this is an
// unparsed region.
let isVersioned =
(try? ifConfigClause.isVersioned(
configuration: configuration,
diagnosticHandler: diagnosticHandler
)) ?? true
if isVersioned {
return .unparsed
}

currentState = .inactive
}
}

currentNode = parent
}

return currentState
}

/// Determine whether the given syntax node is active given a set of
/// configured regions as produced by `configuredRegions(in:)`.
///
/// This is
/// an approximation
public func isActive(
inConfiguredRegions regions: [(IfConfigClauseSyntax, ConfiguredRegionState)]
) -> ConfiguredRegionState {
var currentState: ConfiguredRegionState = .active
for (ifClause, state) in regions {
if self.position < ifClause.position {
return currentState
}

if self.position <= ifClause.endPosition {
currentState = state
}
}

return currentState
}
}

extension IfConfigClauseSyntax {
/// Determine whether this condition is "versioned".
func isVersioned(
Expand Down
Loading

0 comments on commit aa97fbd

Please sign in to comment.