Skip to content

Commit 65fd3b9

Browse files
committed
Handle errors while removing inactive regions
When removing inactive regions via a visitor, accumulate any diagnostics produced during this process so they can be reported. Introduce a test harness so we can test these diagnostics more easily.
1 parent 5428bbc commit 65fd3b9

File tree

10 files changed

+264
-55
lines changed

10 files changed

+264
-55
lines changed

Package.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,12 @@ let package = Package(
152152

153153
.testTarget(
154154
name: "SwiftIfConfigTest",
155-
dependencies: ["_SwiftSyntaxTestSupport", "SwiftIfConfig", "SwiftParser"]
155+
dependencies: [
156+
"_SwiftSyntaxTestSupport",
157+
"SwiftIfConfig",
158+
"SwiftParser",
159+
"SwiftSyntaxMacrosGenericTestSupport",
160+
]
156161
),
157162

158163
// MARK: SwiftLexicalLookup

Sources/SwiftIfConfig/IfConfigEvaluation.swift

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import SwiftOperators
12
//===----------------------------------------------------------------------===//
23
//
34
// This source file is part of the Swift.org open source project
@@ -10,7 +11,6 @@
1011
//
1112
//===----------------------------------------------------------------------===//
1213
import SwiftSyntax
13-
import SwiftOperators
1414

1515
enum IfConfigError: Error, CustomStringConvertible {
1616
case unknownExpression(ExprSyntax)
@@ -196,15 +196,15 @@ private func evaluateIfConfig(
196196

197197
// Logical '!'.
198198
if let prefixOp = condition.as(PrefixOperatorExprSyntax.self),
199-
prefixOp.operator.text == "!"
199+
prefixOp.operator.text == "!"
200200
{
201201
return try !evaluateIfConfig(condition: prefixOp.expression, configuration: configuration)
202202
}
203203

204204
// Logical '&&' and '||'.
205205
if let binOp = condition.as(InfixOperatorExprSyntax.self),
206-
let op = binOp.operator.as(BinaryOperatorExprSyntax.self),
207-
(op.operator.text == "&&" || op.operator.text == "||")
206+
let op = binOp.operator.as(BinaryOperatorExprSyntax.self),
207+
(op.operator.text == "&&" || op.operator.text == "||")
208208
{
209209
// Evaluate the left-hand side.
210210
let lhsResult = try evaluateIfConfig(condition: binOp.leftOperand, configuration: configuration)
@@ -290,7 +290,10 @@ private func evaluateIfConfig(
290290
return try doSingleIdentifierArgumentCheck(configuration.isActiveTargetRuntime, role: "runtime")
291291

292292
case ._ptrauth:
293-
return try doSingleIdentifierArgumentCheck(configuration.isActiveTargetPointerAuthentication, role: "pointer authentication scheme")
293+
return try doSingleIdentifierArgumentCheck(
294+
configuration.isActiveTargetPointerAuthentication,
295+
role: "pointer authentication scheme"
296+
)
294297

295298
case ._endian:
296299
// Ensure that we have a single argument that is a simple identifier,
@@ -313,7 +316,10 @@ private func evaluateIfConfig(
313316
argFirst == "_",
314317
let expectedPointerBitWidth = Int(arg.dropFirst())
315318
else {
316-
throw IfConfigError.requiresUnlabeledArgument(name: fnName, role: "pointer bit with ('_' followed by an integer)")
319+
throw IfConfigError.requiresUnlabeledArgument(
320+
name: fnName,
321+
role: "pointer bit with ('_' followed by an integer)"
322+
)
317323
}
318324

319325
return configuration.targetPointerBitWidth == expectedPointerBitWidth

Sources/SwiftIfConfig/IfConfigRewriter.swift

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
//
2020
//===----------------------------------------------------------------------===//
2121

22+
import SwiftDiagnostics
2223
import SwiftSyntax
2324

2425
/// Syntax rewriter that only visits syntax nodes that are active according
@@ -53,11 +54,17 @@ import SwiftSyntax
5354
/// than trivia).
5455
class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
5556
let configuration: Configuration
57+
var diagnostics: [Diagnostic] = []
5658

5759
init(configuration: Configuration) {
5860
self.configuration = configuration
5961
}
6062

63+
private func reportEvaluationError(at node: some SyntaxProtocol, error: Error) {
64+
let newDiagnostics = error.asDiagnostics(at: node)
65+
diagnostics.append(contentsOf: newDiagnostics)
66+
}
67+
6168
private func dropInactive<List: Collection & SyntaxCollection>(
6269
_ node: List,
6370
elementAsIfConfig: (List.Element) -> IfConfigDeclSyntax?
@@ -69,19 +76,30 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
6976

7077
// Find #ifs within the list.
7178
if let ifConfigDecl = elementAsIfConfig(element) {
79+
// Evaluate the `#if` condition.
80+
let activeClause: IfConfigClauseSyntax?
81+
do {
82+
activeClause = try ifConfigDecl.activeClause(in: configuration)
83+
} catch {
84+
// When an error occurs in the evaluation of the condition,
85+
// keep the entire `#if`.
86+
if anyChanged {
87+
newElements.append(element)
88+
}
89+
90+
reportEvaluationError(at: element, error: error)
91+
continue
92+
}
93+
7294
// If this is the first element that changed, note that we have
7395
// changes and add all prior elements to the list of new elements.
7496
if !anyChanged {
7597
anyChanged = true
7698
newElements.append(contentsOf: node[..<elementIndex])
7799
}
78100

79-
// FIXME: Swallowing errors
80-
guard let activeClause = try? ifConfigDecl.activeClause(in: configuration) else {
81-
continue
82-
}
83-
84-
guard let elements = activeClause.elements else {
101+
// Extract the elements from the active clause, if there are any.
102+
guard let elements = activeClause?.elements else {
85103
continue
86104
}
87105

@@ -231,9 +249,15 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
231249
postfixIfConfig: PostfixIfConfigExprSyntax
232250
) -> ExprSyntax {
233251
// Determine the active clause within this syntax node.
234-
// TODO: Swallows errors
235-
guard let activeClause = try? postfixIfConfig.config.activeClause(in: configuration),
236-
case .postfixExpression(let postfixExpr) = activeClause.elements
252+
let activeClause: IfConfigClauseSyntax?
253+
do {
254+
activeClause = try postfixIfConfig.config.activeClause(in: configuration)
255+
} catch {
256+
reportEvaluationError(at: postfixIfConfig, error: error)
257+
return ExprSyntax(postfixIfConfig)
258+
}
259+
260+
guard case .postfixExpression(let postfixExpr) = activeClause?.elements
237261
else {
238262
// If there is no active clause, return the base.
239263

@@ -263,11 +287,15 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
263287
return applyBaseToPostfixExpression(base: base, postfix: postfixExpr)
264288
}
265289

266-
// TODO: PostfixIfConfigExprSyntax has a different form that doesn't work
290+
// FIXME: PostfixIfConfigExprSyntax has a different form that doesn't work
267291
// well with the way dropInactive is written. We essentially need to
268292
// thread a the "base" into the active clause.
269293
override func visit(_ node: PostfixIfConfigExprSyntax) -> ExprSyntax {
270294
let rewrittenNode = dropInactive(outerBase: nil, postfixIfConfig: node)
295+
if rewrittenNode == ExprSyntax(node) {
296+
return rewrittenNode
297+
}
298+
271299
return visit(rewrittenNode)
272300
}
273301
}
@@ -276,8 +304,11 @@ extension SyntaxProtocol {
276304
/// Produce a copy of this syntax node that removes all syntax regions that
277305
/// are inactive according to the given build configuration, leaving only
278306
/// the code that is active within that build configuration.
279-
public func removingInactive(in configuration: some BuildConfiguration) -> Syntax {
307+
///
308+
/// Returns the syntax node with all inactive regions removed, along with an
309+
/// array containing any diagnostics produced along the way.
310+
public func removingInactive(in configuration: some BuildConfiguration) -> (Syntax, [Diagnostic]) {
280311
let visitor = ActiveSyntaxRewriter(configuration: configuration)
281-
return visitor.rewrite(Syntax(self))
312+
return (visitor.rewrite(Syntax(self)), visitor.diagnostics)
282313
}
283314
}

Sources/SwiftIfConfig/IfConfigVisitor.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ open class ActiveSyntaxVisitor<Configuration: BuildConfiguration>: SyntaxVisitor
7070
do {
7171
// If there is an active clause, visit it's children.
7272
if let activeClause = try node.activeClause(in: configuration),
73-
let elements = activeClause.elements {
73+
let elements = activeClause.elements
74+
{
7475
walk(Syntax(elements))
7576
}
7677

@@ -144,7 +145,8 @@ open class ActiveSyntaxAnyVisitor<Configuration: BuildConfiguration>: SyntaxAnyV
144145
do {
145146
// If there is an active clause, visit it's children.
146147
if let activeClause = try node.activeClause(in: configuration),
147-
let elements = activeClause.elements {
148+
let elements = activeClause.elements
149+
{
148150
walk(Syntax(elements))
149151
}
150152

Sources/SwiftIfConfig/SyntaxLiteralUtils.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ extension ExprSyntax {
3636
/// Whether this is a simple identifier expression and, if so, what the identifier string is.
3737
var simpleIdentifierExpr: String? {
3838
guard let identExpr = self.as(DeclReferenceExprSyntax.self),
39-
identExpr.argumentNames == nil
39+
identExpr.argumentNames == nil
4040
else {
4141
return nil
4242
}

Sources/SwiftSyntaxMacrosGenericTestSupport/Assertions.swift

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ public struct NoteSpec {
146146

147147
func assertNote(
148148
_ note: Note,
149-
in expansionContext: BasicMacroExpansionContext,
149+
in expansionContext: DiagnosticAssertionContext,
150150
expected spec: NoteSpec,
151151
failureHandler: (TestFailureSpec) -> Void
152152
) {
@@ -338,9 +338,36 @@ extension DiagnosticSpec {
338338
}
339339
}
340340

341-
func assertDiagnostic(
341+
/// Describes the context in which we are asserting diagnostic correctness.
342+
///
343+
/// This is used to map source locations.
344+
public enum DiagnosticAssertionContext {
345+
case macroExpansion(BasicMacroExpansionContext)
346+
case tree(any SyntaxProtocol)
347+
348+
func location(
349+
for position: AbsolutePosition,
350+
anchoredAt node: Syntax,
351+
fileName: String
352+
) -> SourceLocation {
353+
switch self {
354+
case .macroExpansion(let expansionContext):
355+
return expansionContext.location(
356+
for: position,
357+
anchoredAt: node,
358+
fileName: fileName
359+
)
360+
361+
case .tree(let syntax):
362+
return SourceLocationConverter(fileName: fileName, tree: syntax)
363+
.location(for: position)
364+
}
365+
}
366+
}
367+
368+
public func assertDiagnostic(
342369
_ diag: Diagnostic,
343-
in expansionContext: BasicMacroExpansionContext,
370+
in expansionContext: DiagnosticAssertionContext,
344371
expected spec: DiagnosticSpec,
345372
failureHandler: (TestFailureSpec) -> Void
346373
) {
@@ -533,7 +560,12 @@ public func assertMacroExpansion(
533560
)
534561
} else {
535562
for (actualDiag, expectedDiag) in zip(context.diagnostics, diagnostics) {
536-
assertDiagnostic(actualDiag, in: context, expected: expectedDiag, failureHandler: failureHandler)
563+
assertDiagnostic(
564+
actualDiag,
565+
in: .macroExpansion(context),
566+
expected: expectedDiag,
567+
failureHandler: failureHandler
568+
)
537569
}
538570
}
539571

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
import SwiftDiagnostics
13+
import SwiftIfConfig
14+
import SwiftParser
15+
import SwiftSyntax
16+
@_spi(XCTestFailureLocation) import SwiftSyntaxMacrosGenericTestSupport
17+
import XCTest
18+
import _SwiftSyntaxGenericTestSupport
19+
import _SwiftSyntaxTestSupport
20+
21+
/// Assert that applying the given build configuration to the source code
22+
/// returns the expected source and diagnostics.
23+
func assertRemoveInactive(
24+
_ source: String,
25+
configuration: any BuildConfiguration,
26+
diagnostics expectedDiagnostics: [DiagnosticSpec] = [],
27+
expectedSource: String,
28+
file: StaticString = #filePath,
29+
line: UInt = #line
30+
) {
31+
var parser = Parser(source)
32+
let tree = SourceFileSyntax.parse(from: &parser)
33+
34+
let (treeWithoutInactive, actualDiagnostics) = tree.removingInactive(in: configuration)
35+
36+
// Check the resulting tree.
37+
assertStringsEqualWithDiff(
38+
treeWithoutInactive.description,
39+
expectedSource,
40+
file: file,
41+
line: line
42+
)
43+
44+
// Check the diagnostics.
45+
if actualDiagnostics.count != expectedDiagnostics.count {
46+
XCTFail(
47+
"""
48+
Expected \(expectedDiagnostics.count) diagnostics, but got \(actualDiagnostics.count):
49+
\(actualDiagnostics.map(\.debugDescription).joined(separator: "\n"))
50+
""",
51+
file: file,
52+
line: line
53+
)
54+
} else {
55+
for (actualDiag, expectedDiag) in zip(actualDiagnostics, expectedDiagnostics) {
56+
assertDiagnostic(
57+
actualDiag,
58+
in: .tree(tree),
59+
expected: expectedDiag,
60+
failureHandler: {
61+
XCTFail($0.message, file: $0.location.staticFilePath, line: $0.location.unsignedLine)
62+
}
63+
)
64+
}
65+
}
66+
}

Tests/SwiftIfConfigTest/EvaluateTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import SwiftIfConfig
2+
import SwiftParser
3+
import SwiftSyntax
14
//===----------------------------------------------------------------------===//
25
//
36
// This source file is part of the Swift.org open source project
@@ -10,9 +13,6 @@
1013
//
1114
//===----------------------------------------------------------------------===//
1215
import XCTest
13-
import SwiftSyntax
14-
import SwiftParser
15-
import SwiftIfConfig
1616
import _SwiftSyntaxTestSupport
1717

1818
public class EvaluateTests: XCTestCase {

Tests/SwiftIfConfigTest/TestingBuildConfiguration.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ enum BuildConfigurationError: Error, CustomStringConvertible {
1818
var description: String {
1919
switch self {
2020
case .badAttribute(let attribute):
21-
return "unacceptable attribute \(attribute)"
21+
return "unacceptable attribute '\(attribute)'"
2222
}
2323
}
2424
}

0 commit comments

Comments
 (0)