Skip to content

Implementation of the compiler's "extract inlinable text" #2832

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 146 additions & 6 deletions Sources/SwiftIfConfig/ActiveSyntaxRewriter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,39 @@ extension SyntaxProtocol {
/// 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.
/// - Parameters:
/// - configuration: the configuration to apply.
/// - Returns: the syntax node with all inactive regions removed, along with
/// an array containing any diagnostics produced along the way.
public func removingInactive(
in configuration: some BuildConfiguration
) -> (result: Syntax, diagnostics: [Diagnostic]) {
return removingInactive(in: configuration, retainFeatureCheckIfConfigs: false)
}

/// 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.
///
/// 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.
/// - Parameters:
/// - configuration: the configuration to apply.
/// - retainFeatureCheckIfConfigs: whether to retain `#if` blocks involving
/// compiler version checks (e.g., `compiler(>=6.0)`) and `$`-based
/// feature checks.
/// - Returns: the syntax node with all inactive regions removed, along with
/// an array containing any diagnostics produced along the way.
@_spi(Compiler)
public func removingInactive(
in configuration: some BuildConfiguration,
retainFeatureCheckIfConfigs: Bool
) -> (result: Syntax, diagnostics: [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
Expand All @@ -41,7 +65,10 @@ extension SyntaxProtocol {

// Second pass: Rewrite the syntax tree by removing the inactive clauses
// from each #if (along with the #ifs themselves).
let rewriter = ActiveSyntaxRewriter(configuration: configuration)
let rewriter = ActiveSyntaxRewriter(
configuration: configuration,
retainFeatureCheckIfConfigs: retainFeatureCheckIfConfigs
)
return (
rewriter.rewrite(Syntax(self)),
visitor.diagnostics
Expand Down Expand Up @@ -83,8 +110,12 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
let configuration: Configuration
var diagnostics: [Diagnostic] = []

init(configuration: Configuration) {
/// Whether to retain `#if` blocks containing compiler and feature checks.
var retainFeatureCheckIfConfigs: Bool

init(configuration: Configuration, retainFeatureCheckIfConfigs: Bool) {
self.configuration = configuration
self.retainFeatureCheckIfConfigs = retainFeatureCheckIfConfigs
}

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

// Find #ifs within the list.
if let ifConfigDecl = elementAsIfConfig(element) {
if let ifConfigDecl = elementAsIfConfig(element),
(!retainFeatureCheckIfConfigs || !ifConfigDecl.containsFeatureCheck)
{
// Retrieve the active `#if` clause
let (activeClause, localDiagnostics) = ifConfigDecl.activeClause(in: configuration)

Expand Down Expand Up @@ -262,6 +295,12 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
outerBase: ExprSyntax?,
postfixIfConfig: PostfixIfConfigExprSyntax
) -> ExprSyntax {
// If we're supposed to retain #if configs that are feature checks, and
// this configuration has one, do so.
if retainFeatureCheckIfConfigs && postfixIfConfig.config.containsFeatureCheck {
return ExprSyntax(postfixIfConfig)
}

// Retrieve the active `if` clause.
let (activeClause, localDiagnostics) = postfixIfConfig.config.activeClause(in: configuration)

Expand Down Expand Up @@ -307,3 +346,104 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
return visit(rewrittenNode)
}
}

/// Helper class to find a feature or compiler check.
fileprivate class FindFeatureCheckVisitor: SyntaxVisitor {
var foundFeatureCheck = false

override func visit(_ node: DeclReferenceExprSyntax) -> SyntaxVisitorContinueKind {
// Checks that start with $ are feature checks that should be retained.
if let identifier = node.simpleIdentifier,
let initialChar = identifier.name.first,
initialChar == "$"
{
foundFeatureCheck = true
return .skipChildren
}

return .visitChildren
}

override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind {
if let calleeDeclRef = node.calledExpression.as(DeclReferenceExprSyntax.self),
let calleeName = calleeDeclRef.simpleIdentifier?.name,
(calleeName == "compiler" || calleeName == "_compiler_version")
{
foundFeatureCheck = true
}

return .skipChildren
}
}

extension ExprSyntaxProtocol {
/// Whether any of the nodes in this expression involve compiler or feature
/// checks.
fileprivate var containsFeatureCheck: Bool {
let visitor = FindFeatureCheckVisitor(viewMode: .fixedUp)
visitor.walk(self)
return visitor.foundFeatureCheck
}
}

extension IfConfigDeclSyntax {
/// Whether any of the clauses in this #if contain a feature check.
var containsFeatureCheck: Bool {
return clauses.contains { clause in
if let condition = clause.condition {
return condition.containsFeatureCheck
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just checking: If we have the following we don’t want to remove secretCode right?

#if SECRET
secretCode()
#elseif compiler(>=6.0)
swift6Code()
#else
swift5Code()
#endif

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh my, I really screwed that up. Thank you!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, now I'm looking back at this more closely. I thought I got the basic algorithm wrong, but I did not: this matches what the compiler is doing today, where secretCode() is not removed. We could decide to do better than the compiler here if we want, and it probably wouldn't break anything.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that might be worth it 🤔

} else {
return false
}
}
}
}

extension SyntaxProtocol {
// Produce the source code for this syntax node with all of the comments
// and #sourceLocations removed. Each comment will be replaced with either
// a newline or a space, depending on whether the comment involved a newline.
@_spi(Compiler)
public var descriptionWithoutCommentsAndSourceLocations: String {
var result = ""
var skipUntilRParen = false
for token in tokens(viewMode: .sourceAccurate) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SyntaxVisitor is probably faster because it uses SyntaxNodeFactory hack.

class DescriptionWithoutCommentsAndSourceLocationsVisotr: SyntaxVisitor {
  var result: String = ""
  override func visit(_ token: TokenSyntax) -> SyntaxVisitorContinueKind {
    token.leadingTrivia.writeWithoutComments(to: &result)
    token.text.write(to: &result)
    token.trailingTrivia.writeWithoutComments(to: &result)
    return .skipChildren
  }
  override func visit(_ node: PoundSourceLocationSyntax) -> SyntaxVisitorContinueKind {
    return .skipChildren
  }
}

Copy link
Member Author

@DougGregor DougGregor Sep 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is much cleaner than my implementation, too! I will steal this for a follow-up PR.

// Skip #sourceLocation(...).
if token.tokenKind == .poundSourceLocation {
skipUntilRParen = true
continue
}

if skipUntilRParen {
if token.tokenKind == .rightParen {
skipUntilRParen = false
}
continue
}

token.leadingTrivia.writeWithoutComments(to: &result)
token.text.write(to: &result)
token.trailingTrivia.writeWithoutComments(to: &result)
}
return result
}
}

extension Trivia {
fileprivate func writeWithoutComments(to stream: inout some TextOutputStream) {
for piece in pieces {
switch piece {
case .backslashes, .carriageReturnLineFeeds, .carriageReturns, .formfeeds, .newlines, .pounds, .spaces, .tabs,
.unexpectedText, .verticalTabs:
piece.write(to: &stream)

case .blockComment(let text), .docBlockComment(let text), .docLineComment(let text), .lineComment(let text):
if text.contains(where: \.isNewline) {
stream.write("\n")
} else {
stream.write(" ")
}
}
}
}
}
1 change: 1 addition & 0 deletions Sources/SwiftIfConfig/BuildConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import SwiftSyntax

/// Describes the ordering of a sequence of bytes that make up a word of
Expand Down
15 changes: 7 additions & 8 deletions Sources/SwiftIfConfig/ConfiguredRegions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import SwiftSyntax
/// - Unparsed region for the `#elseif compiler(>= 12.0)`.
/// - Inactive region for the final `#else`.
public struct ConfiguredRegions {
let regions: [Element]
let regions: [(ifClause: IfConfigClauseSyntax, state: IfConfigRegionState)]

/// The set of diagnostics produced when evaluating the configured regions.
public let diagnostics: [Diagnostic]
Expand All @@ -59,13 +59,13 @@ public struct ConfiguredRegions {
let middle = currentSlice.startIndex + currentSlice.count / 2

// If the node is prior to the start of the middle, take the left-hand side.
if node.position < currentSlice[middle].0.regionStart {
if node.position < currentSlice[middle].ifClause.regionStart {
currentSlice = currentSlice[..<middle]
continue
}

// If the node is after the end of the middle, take the right-hand side.
if node.position > currentSlice[middle].0.endPosition {
if node.position > currentSlice[middle].ifClause.endPosition {
currentSlice = currentSlice[(middle + 1)...]
continue
}
Expand All @@ -77,13 +77,13 @@ public struct ConfiguredRegions {
// Find the last region in which this node lands. If there is no such
// region, this is active.
return currentSlice.last { region in
node.position >= region.0.regionStart && node.position <= region.0.endPosition
}?.1 ?? .active
(region.ifClause.regionStart...region.ifClause.endPosition).contains(node.position)
}?.state ?? .active
}
}

extension ConfiguredRegions: RandomAccessCollection {
public typealias Element = (IfConfigClauseSyntax, IfConfigRegionState)
public typealias Element = (ifClause: IfConfigClauseSyntax, state: IfConfigRegionState)
public var startIndex: Int { regions.startIndex }
public var endIndex: Int { regions.endIndex }

Expand All @@ -99,7 +99,7 @@ extension ConfiguredRegions: CustomDebugStringConvertible {
return "[]"
}

let root = firstRegion.0.root
let root = firstRegion.ifClause.root
let converter = SourceLocationConverter(fileName: "", tree: root)
let regionDescriptions = regions.map { (ifClause, state) in
let startPosition = converter.location(for: ifClause.position)
Expand Down Expand Up @@ -198,7 +198,6 @@ fileprivate class ConfiguredRegionVisitor<Configuration: BuildConfiguration>: Sy

// In an active region, evaluate the condition to determine whether
// this clause is active. Otherwise, this clause is inactive.
// inactive.
if inActiveRegion {
let (thisIsActive, _, evalDiagnostics) = evaluateIfConfig(
condition: foldedCondition,
Expand Down
6 changes: 1 addition & 5 deletions Sources/SwiftIfConfig/SwiftIfConfig.docc/SwiftIfConfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,4 @@ The `SwiftIfConfig` library provides utilities to determine which syntax nodes a
* <doc:ActiveSyntaxVisitor> and <doc:ActiveSyntaxAnyVisitor> are visitor types that only visit the syntax nodes that are included ("active") for a given build configuration, implicitly skipping any nodes within inactive `#if` clauses.
* `SyntaxProtocol.removingInactive(in:)` produces a syntax node that removes all inactive regions (and their corresponding `IfConfigDeclSyntax` nodes) from the given syntax tree, returning a new tree that is free of `#if` conditions.
* `IfConfigDeclSyntax.activeClause(in:)` determines which of the clauses of an `#if` is active for the given build configuration, returning the active clause.
* `SyntaxProtocol.isActive(in:)` determines whether the given syntax node is active for the given build configuration. The result is one of "active"
(the node is included in the program), "inactive" (the node is not included
in the program), or "unparsed" (the node is not included in the program and
is also allowed to have syntax errors).
* `SyntaxProtocol.configuredRegions(in:)` produces an array describing the various regions in which a configuration has an effect, indicating active, inactive, and unparsed regions in the source tree. The array can be used as an input to `SyntaxProtocol.isActive(inConfiguredRegions:)` to determine whether a given syntax node is active.
* `SyntaxProtocol.configuredRegions(in:)` produces a `ConfiguredRegions` value that can be used to efficiently test whether a given syntax node is in an active, inactive, or unparsed region (via `isActive`).
13 changes: 0 additions & 13 deletions Sources/SwiftIfConfig/SyntaxProtocol+IfConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,4 @@ extension SyntaxProtocol {
let configuredRegions = root.configuredRegions(in: configuration)
return (configuredRegions.isActive(self), configuredRegions.diagnostics)
}

/// Determine whether the given syntax node is active given a set of
/// configured regions as produced by `configuredRegions(in:)`.
///
/// If you are querying whether many syntax nodes in a particular file are
/// active, consider calling `configuredRegions(in:)` once and using
/// this function. For occasional queries, use `isActive(in:)`.
@available(*, deprecated, message: "Please use ConfiguredRegions.isActive(_:)")
public func isActive(
inConfiguredRegions regions: ConfiguredRegions
) -> IfConfigRegionState {
regions.isActive(self)
}
}
Loading