@@ -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+ }
0 commit comments