-
Notifications
You must be signed in to change notification settings - Fork 439
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
Changes from all commits
80f3a34
45bf2b8
392167b
7036a41
2115d77
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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 | ||
|
@@ -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>( | ||
|
@@ -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) | ||
|
||
|
@@ -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) | ||
|
||
|
@@ -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 | ||
} 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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
}
} There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(" ") | ||
} | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
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?There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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 🤔