@@ -18,15 +18,39 @@ extension SyntaxProtocol {
18
18
/// are inactive according to the given build configuration, leaving only
19
19
/// the code that is active within that build configuration.
20
20
///
21
- /// Returns the syntax node with all inactive regions removed, along with an
22
- /// array containing any diagnostics produced along the way.
23
- ///
24
21
/// If there are errors in the conditions of any configuration
25
22
/// clauses, e.g., `#if FOO > 10`, then the condition will be
26
23
/// considered to have failed and the clauses's elements will be
27
24
/// 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.
28
29
public func removingInactive(
29
30
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
30
54
) -> ( result: Syntax , diagnostics: [ Diagnostic ] ) {
31
55
// First pass: Find all of the active clauses for the #ifs we need to
32
56
// visit, along with any diagnostics produced along the way. This process
@@ -41,7 +65,10 @@ extension SyntaxProtocol {
41
65
42
66
// Second pass: Rewrite the syntax tree by removing the inactive clauses
43
67
// 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
+ )
45
72
return (
46
73
rewriter. rewrite ( Syntax ( self ) ) ,
47
74
visitor. diagnostics
@@ -83,8 +110,12 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
83
110
let configuration : Configuration
84
111
var diagnostics : [ Diagnostic ] = [ ]
85
112
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 ) {
87
117
self . configuration = configuration
118
+ self . retainFeatureCheckIfConfigs = retainFeatureCheckIfConfigs
88
119
}
89
120
90
121
private func dropInactive< List: SyntaxCollection > (
@@ -97,7 +128,9 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
97
128
let element = node [ elementIndex]
98
129
99
130
// Find #ifs within the list.
100
- if let ifConfigDecl = elementAsIfConfig ( element) {
131
+ if let ifConfigDecl = elementAsIfConfig ( element) ,
132
+ ( !retainFeatureCheckIfConfigs || !ifConfigDecl. containsFeatureCheck)
133
+ {
101
134
// Retrieve the active `#if` clause
102
135
let ( activeClause, localDiagnostics) = ifConfigDecl. activeClause ( in: configuration)
103
136
@@ -262,6 +295,12 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
262
295
outerBase: ExprSyntax ? ,
263
296
postfixIfConfig: PostfixIfConfigExprSyntax
264
297
) -> 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
+
265
304
// Retrieve the active `if` clause.
266
305
let ( activeClause, localDiagnostics) = postfixIfConfig. config. activeClause ( in: configuration)
267
306
@@ -307,3 +346,104 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
307
346
return visit ( rewrittenNode)
308
347
}
309
348
}
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
+ }
0 commit comments