Skip to content

Commit c30bd36

Browse files
authored
Merge pull request #2832 from DougGregor/extract-inlinable-text
Implementation of the compiler's "extract inlinable text"
2 parents 2b69380 + 2115d77 commit c30bd36

File tree

2 files changed

+234
-8
lines changed

2 files changed

+234
-8
lines changed

Sources/SwiftIfConfig/ActiveSyntaxRewriter.swift

Lines changed: 146 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,39 @@ extension SyntaxProtocol {
1818
/// are inactive according to the given build configuration, leaving only
1919
/// the code that is active within that build configuration.
2020
///
21-
/// Returns the syntax node with all inactive regions removed, along with an
22-
/// array containing any diagnostics produced along the way.
23-
///
2421
/// If there are errors in the conditions of any configuration
2522
/// clauses, e.g., `#if FOO > 10`, then the condition will be
2623
/// considered to have failed and the clauses's elements will be
2724
/// removed.
25+
/// - Parameters:
26+
/// - configuration: the configuration to apply.
27+
/// - Returns: the syntax node with all inactive regions removed, along with
28+
/// an array containing any diagnostics produced along the way.
2829
public func removingInactive(
2930
in configuration: some BuildConfiguration
31+
) -> (result: Syntax, diagnostics: [Diagnostic]) {
32+
return removingInactive(in: configuration, retainFeatureCheckIfConfigs: false)
33+
}
34+
35+
/// Produce a copy of this syntax node that removes all syntax regions that
36+
/// are inactive according to the given build configuration, leaving only
37+
/// the code that is active within that build configuration.
38+
///
39+
/// If there are errors in the conditions of any configuration
40+
/// clauses, e.g., `#if FOO > 10`, then the condition will be
41+
/// considered to have failed and the clauses's elements will be
42+
/// removed.
43+
/// - Parameters:
44+
/// - configuration: the configuration to apply.
45+
/// - retainFeatureCheckIfConfigs: whether to retain `#if` blocks involving
46+
/// compiler version checks (e.g., `compiler(>=6.0)`) and `$`-based
47+
/// feature checks.
48+
/// - Returns: the syntax node with all inactive regions removed, along with
49+
/// an array containing any diagnostics produced along the way.
50+
@_spi(Compiler)
51+
public func removingInactive(
52+
in configuration: some BuildConfiguration,
53+
retainFeatureCheckIfConfigs: Bool
3054
) -> (result: Syntax, diagnostics: [Diagnostic]) {
3155
// First pass: Find all of the active clauses for the #ifs we need to
3256
// visit, along with any diagnostics produced along the way. This process
@@ -41,7 +65,10 @@ extension SyntaxProtocol {
4165

4266
// Second pass: Rewrite the syntax tree by removing the inactive clauses
4367
// from each #if (along with the #ifs themselves).
44-
let rewriter = ActiveSyntaxRewriter(configuration: configuration)
68+
let rewriter = ActiveSyntaxRewriter(
69+
configuration: configuration,
70+
retainFeatureCheckIfConfigs: retainFeatureCheckIfConfigs
71+
)
4572
return (
4673
rewriter.rewrite(Syntax(self)),
4774
visitor.diagnostics
@@ -83,8 +110,12 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
83110
let configuration: Configuration
84111
var diagnostics: [Diagnostic] = []
85112

86-
init(configuration: Configuration) {
113+
/// Whether to retain `#if` blocks containing compiler and feature checks.
114+
var retainFeatureCheckIfConfigs: Bool
115+
116+
init(configuration: Configuration, retainFeatureCheckIfConfigs: Bool) {
87117
self.configuration = configuration
118+
self.retainFeatureCheckIfConfigs = retainFeatureCheckIfConfigs
88119
}
89120

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

99130
// Find #ifs within the list.
100-
if let ifConfigDecl = elementAsIfConfig(element) {
131+
if let ifConfigDecl = elementAsIfConfig(element),
132+
(!retainFeatureCheckIfConfigs || !ifConfigDecl.containsFeatureCheck)
133+
{
101134
// Retrieve the active `#if` clause
102135
let (activeClause, localDiagnostics) = ifConfigDecl.activeClause(in: configuration)
103136

@@ -262,6 +295,12 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
262295
outerBase: ExprSyntax?,
263296
postfixIfConfig: PostfixIfConfigExprSyntax
264297
) -> ExprSyntax {
298+
// If we're supposed to retain #if configs that are feature checks, and
299+
// this configuration has one, do so.
300+
if retainFeatureCheckIfConfigs && postfixIfConfig.config.containsFeatureCheck {
301+
return ExprSyntax(postfixIfConfig)
302+
}
303+
265304
// Retrieve the active `if` clause.
266305
let (activeClause, localDiagnostics) = postfixIfConfig.config.activeClause(in: configuration)
267306

@@ -307,3 +346,104 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
307346
return visit(rewrittenNode)
308347
}
309348
}
349+
350+
/// Helper class to find a feature or compiler check.
351+
fileprivate class FindFeatureCheckVisitor: SyntaxVisitor {
352+
var foundFeatureCheck = false
353+
354+
override func visit(_ node: DeclReferenceExprSyntax) -> SyntaxVisitorContinueKind {
355+
// Checks that start with $ are feature checks that should be retained.
356+
if let identifier = node.simpleIdentifier,
357+
let initialChar = identifier.name.first,
358+
initialChar == "$"
359+
{
360+
foundFeatureCheck = true
361+
return .skipChildren
362+
}
363+
364+
return .visitChildren
365+
}
366+
367+
override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind {
368+
if let calleeDeclRef = node.calledExpression.as(DeclReferenceExprSyntax.self),
369+
let calleeName = calleeDeclRef.simpleIdentifier?.name,
370+
(calleeName == "compiler" || calleeName == "_compiler_version")
371+
{
372+
foundFeatureCheck = true
373+
}
374+
375+
return .skipChildren
376+
}
377+
}
378+
379+
extension ExprSyntaxProtocol {
380+
/// Whether any of the nodes in this expression involve compiler or feature
381+
/// checks.
382+
fileprivate var containsFeatureCheck: Bool {
383+
let visitor = FindFeatureCheckVisitor(viewMode: .fixedUp)
384+
visitor.walk(self)
385+
return visitor.foundFeatureCheck
386+
}
387+
}
388+
389+
extension IfConfigDeclSyntax {
390+
/// Whether any of the clauses in this #if contain a feature check.
391+
var containsFeatureCheck: Bool {
392+
return clauses.contains { clause in
393+
if let condition = clause.condition {
394+
return condition.containsFeatureCheck
395+
} else {
396+
return false
397+
}
398+
}
399+
}
400+
}
401+
402+
extension SyntaxProtocol {
403+
// Produce the source code for this syntax node with all of the comments
404+
// and #sourceLocations removed. Each comment will be replaced with either
405+
// a newline or a space, depending on whether the comment involved a newline.
406+
@_spi(Compiler)
407+
public var descriptionWithoutCommentsAndSourceLocations: String {
408+
var result = ""
409+
var skipUntilRParen = false
410+
for token in tokens(viewMode: .sourceAccurate) {
411+
// Skip #sourceLocation(...).
412+
if token.tokenKind == .poundSourceLocation {
413+
skipUntilRParen = true
414+
continue
415+
}
416+
417+
if skipUntilRParen {
418+
if token.tokenKind == .rightParen {
419+
skipUntilRParen = false
420+
}
421+
continue
422+
}
423+
424+
token.leadingTrivia.writeWithoutComments(to: &result)
425+
token.text.write(to: &result)
426+
token.trailingTrivia.writeWithoutComments(to: &result)
427+
}
428+
return result
429+
}
430+
}
431+
432+
extension Trivia {
433+
fileprivate func writeWithoutComments(to stream: inout some TextOutputStream) {
434+
for piece in pieces {
435+
switch piece {
436+
case .backslashes, .carriageReturnLineFeeds, .carriageReturns, .formfeeds, .newlines, .pounds, .spaces, .tabs,
437+
.unexpectedText, .verticalTabs:
438+
piece.write(to: &stream)
439+
440+
case .blockComment(let text), .docBlockComment(let text), .docLineComment(let text), .lineComment(let text):
441+
if text.contains(where: \.isNewline) {
442+
stream.write("\n")
443+
} else {
444+
stream.write(" ")
445+
}
446+
}
447+
}
448+
}
449+
}

Tests/SwiftIfConfigTest/VisitorTests.swift

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

1313
import SwiftDiagnostics
14-
import SwiftIfConfig
14+
@_spi(Compiler) import SwiftIfConfig
1515
import SwiftParser
1616
import SwiftSyntax
1717
@_spi(XCTestFailureLocation) @_spi(Testing) import SwiftSyntaxMacrosGenericTestSupport
@@ -253,13 +253,96 @@ public class VisitorTests: XCTestCase {
253253
"""
254254
)
255255
}
256+
257+
func testRemoveInactiveRetainingFeatureChecks() {
258+
assertRemoveInactive(
259+
"""
260+
public func hasIfCompilerCheck(_ x: () -> Bool = {
261+
#if compiler(>=5.3)
262+
return true
263+
#else
264+
return false
265+
#endif
266+
267+
#if $Blah
268+
return 0
269+
#else
270+
return 1
271+
#endif
272+
273+
#if NOT_SET
274+
return 3.14159
275+
#else
276+
return 2.71828
277+
#endif
278+
}) {
279+
}
280+
""",
281+
configuration: linuxBuildConfig,
282+
retainFeatureCheckIfConfigs: true,
283+
expectedSource: """
284+
public func hasIfCompilerCheck(_ x: () -> Bool = {
285+
#if compiler(>=5.3)
286+
return true
287+
#else
288+
return false
289+
#endif
290+
291+
#if $Blah
292+
return 0
293+
#else
294+
return 1
295+
#endif
296+
return 2.71828
297+
}) {
298+
}
299+
"""
300+
)
301+
}
302+
303+
func testRemoveCommentsAndSourceLocations() {
304+
let original: SourceFileSyntax = """
305+
306+
/// This is a documentation comment
307+
func f() { }
308+
309+
#sourceLocation(file: "if-configs.swift", line: 200)
310+
/** Another documentation comment
311+
that is split across
312+
multiple lines */
313+
func g() { }
314+
315+
func h() {
316+
x +/*comment*/y
317+
// foo
318+
}
319+
"""
320+
321+
assertStringsEqualWithDiff(
322+
original.descriptionWithoutCommentsAndSourceLocations,
323+
"""
324+
325+
326+
func f() { }
327+
328+
329+
func g() { }
330+
331+
func h() {
332+
x + y
333+
334+
}
335+
"""
336+
)
337+
}
256338
}
257339

258340
/// Assert that applying the given build configuration to the source code
259341
/// returns the expected source and diagnostics.
260342
fileprivate func assertRemoveInactive(
261343
_ source: String,
262344
configuration: some BuildConfiguration,
345+
retainFeatureCheckIfConfigs: Bool = false,
263346
diagnostics expectedDiagnostics: [DiagnosticSpec] = [],
264347
expectedSource: String,
265348
file: StaticString = #filePath,
@@ -268,7 +351,10 @@ fileprivate func assertRemoveInactive(
268351
var parser = Parser(source)
269352
let tree = SourceFileSyntax.parse(from: &parser)
270353

271-
let (treeWithoutInactive, actualDiagnostics) = tree.removingInactive(in: configuration)
354+
let (treeWithoutInactive, actualDiagnostics) = tree.removingInactive(
355+
in: configuration,
356+
retainFeatureCheckIfConfigs: retainFeatureCheckIfConfigs
357+
)
272358

273359
// Check the resulting tree.
274360
assertStringsEqualWithDiff(

0 commit comments

Comments
 (0)