From f409d278c5f57d70e0235c759a2d13bd1a69d91c Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 20 Sep 2024 10:37:45 -0400 Subject: [PATCH] Rethink how we capture expectation conditions and their subexpressions. This PR completely rewrites how we capture expectation conditions. For example, given the following expectation: ```swift ``` We currently detect that there is a binary operation and emit code that calls the binary operator as a closure and passes the left-hand value and right-hand value, then checks that the result of the operation is `true`. This is sufficient for simpler expressions like that one, but more complex ones (including any that involve `try` or `await` keywords) cannot be expanded correctly. With this PR, such expressions _can_ generally be expanded correctly. The change involves rewriting the macro condition as a closure to which is passed a local, mutable "context" value. Subexpressions of the condition expression are then rewritten by walking the syntax tree of the expression (using typical swift-syntax API) and replacing them with calls into the context value that pass in the value and related state. If the expectation ultimately fails, the collected data is transformed into an instance of the SPI type `Expression` that contains the source code of the expression and interesting subexpressions as well as the runtime values of those subexpressions. Nodes in the syntax tree are identified by a unique ID which is composed of the swift-syntax ID for that node as well as all its parent nodes in a compact bitmask format. These IDs can be transformed into graph/trie key paths when expression/subexpression relationships need to be reconstructed on failure, meaning that a single rewritten node doesn't otherwise need to know its "place" in the overall expression. There remain a few caveats (that also generally affect the current implementation): - Mutating member functions are syntactically indistinguishable from non-mutating ones and miscompile when rewritten; - Expressions involving move-only types are also indistinguishable, but need lifetime management to be rewritten correctly; and - Expressions where the `try` or `await` keyword is _outside_ the `#expect` macro cannot be expanded correctly because the macro cannot see those keywords during expansion. The first issue might be resolvable in the future using pointer tricks, although I don't hold a lot of hope for it. The second issue is probably resolved by non-escaping types. The third issue is an area of active exploration for us and the macros/swift-syntax team. --- Package.swift | 7 + .../v0/Encoded/ABIv0.EncodedExpectation.swift | 34 + .../v0/Encoded/ABIv0.EncodedExpression.swift | 52 + .../ABI/v0/Encoded/ABIv0.EncodedIssue.swift | 8 + .../ABI/v0/Encoded/ABIv0.EncodedMessage.swift | 8 + Sources/Testing/CMakeLists.txt | 6 +- .../Event.ConsoleOutputRecorder.swift | 40 +- .../Event.HumanReadableOutputRecorder.swift | 29 +- Sources/Testing/ExitTests/ExitTest.swift | 12 +- .../Expectations/Expectation+Macro.swift | 8 +- .../Testing/Expectations/Expectation.swift | 8 +- .../ExpectationChecking+Macro.swift | 825 +++---------- .../ExpectationContext+Pointers.swift | 146 +++ .../Expectations/ExpectationContext.swift | 454 +++++++ .../SourceAttribution/Expression+Macro.swift | 105 -- .../SourceAttribution/Expression.swift | 341 +----- .../SourceAttribution/ExpressionID.swift | 171 +++ .../CollectionDifferenceAdditions.swift | 22 + .../Support/Additions/ResultAdditions.swift | 6 +- Sources/TestingMacros/CMakeLists.txt | 4 +- Sources/TestingMacros/ConditionMacro.swift | 172 ++- .../IntegerLiteralExprSyntaxAdditions.swift | 18 + .../Additions/SyntaxProtocolAdditions.swift | 82 ++ .../Additions/TokenSyntaxAdditions.swift | 10 + .../Support/AttributeDiscovery.swift | 2 +- .../Support/ConditionArgumentParsing.swift | 1089 +++++++++++------ .../Support/EffectfulExpressionHandling.swift | 139 +++ .../Support/SourceCodeCapturing.swift | 117 -- .../TestingMacros/TestDeclarationMacro.swift | 6 +- Sources/TestingMacros/TestingMacrosMain.swift | 1 + .../_TestingInternals/include/TestSupport.h | 4 + .../SubexpressionShowcase.swift | 118 ++ .../ConditionMacroTests.swift | 225 ++-- .../TestSupport/Parse.swift | 1 + Tests/TestingTests/AttachmentTests.swift | 12 +- Tests/TestingTests/EventRecorderTests.swift | 4 +- Tests/TestingTests/EventTests.swift | 1 - Tests/TestingTests/IssueTests.swift | 170 +-- Tests/TestingTests/MiscellaneousTests.swift | 5 + .../Support/CartesianProductTests.swift | 10 +- .../Traits/TimeLimitTraitTests.swift | 5 +- Tests/TestingTests/VariadicGenericTests.swift | 59 +- 42 files changed, 2674 insertions(+), 1862 deletions(-) create mode 100644 Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedExpectation.swift create mode 100644 Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedExpression.swift create mode 100644 Sources/Testing/Expectations/ExpectationContext+Pointers.swift create mode 100644 Sources/Testing/Expectations/ExpectationContext.swift delete mode 100644 Sources/Testing/SourceAttribution/Expression+Macro.swift create mode 100644 Sources/Testing/SourceAttribution/ExpressionID.swift create mode 100644 Sources/TestingMacros/Support/Additions/IntegerLiteralExprSyntaxAdditions.swift create mode 100644 Sources/TestingMacros/Support/Additions/SyntaxProtocolAdditions.swift create mode 100644 Sources/TestingMacros/Support/EffectfulExpressionHandling.swift delete mode 100644 Sources/TestingMacros/Support/SourceCodeCapturing.swift create mode 100644 Tests/SubexpressionShowcase/SubexpressionShowcase.swift diff --git a/Package.swift b/Package.swift index 4a22b98e2..6799b46ee 100644 --- a/Package.swift +++ b/Package.swift @@ -72,6 +72,13 @@ let package = Package( .enableExperimentalFeature("SymbolLinkageMarkers"), ] ), + .testTarget( + name: "SubexpressionShowcase", + dependencies: [ + "Testing", + ], + swiftSettings: .packageSettings + ), .macro( name: "TestingMacros", diff --git a/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedExpectation.swift b/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedExpectation.swift new file mode 100644 index 000000000..4e78859fb --- /dev/null +++ b/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedExpectation.swift @@ -0,0 +1,34 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +extension ABIv0 { + /// A type implementing the JSON encoding of ``Expectation`` for the ABI entry + /// point and event stream output. + /// + /// This type is not part of the public interface of the testing library. It + /// assists in converting values to JSON; clients that consume this JSON are + /// expected to write their own decoders. + /// + /// - Warning: Expectations are not yet part of the JSON schema. + struct EncodedExpectation: Sendable { + /// The expression evaluated by this expectation. + /// + /// - Warning: Expressions are not yet part of the JSON schema. + var _expression: EncodedExpression + + init(encoding expectation: borrowing Expectation, in eventContext: borrowing Event.Context) { + _expression = EncodedExpression(encoding: expectation.evaluatedExpression, in: eventContext) + } + } +} + +// MARK: - Codable + +extension ABIv0.EncodedExpectation: Codable {} diff --git a/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedExpression.swift b/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedExpression.swift new file mode 100644 index 000000000..2a2130ff3 --- /dev/null +++ b/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedExpression.swift @@ -0,0 +1,52 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +extension ABIv0 { + /// A type implementing the JSON encoding of ``Expression`` for the ABI entry + /// point and event stream output. + /// + /// This type is not part of the public interface of the testing library. It + /// assists in converting values to JSON; clients that consume this JSON are + /// expected to write their own decoders. + /// + /// - Warning: Expressions are not yet part of the JSON schema. + struct EncodedExpression: Sendable { + /// The source code of the original captured expression. + var sourceCode: String + + /// A string representation of the runtime value of this expression. + /// + /// If the runtime value of this expression has not been evaluated, the + /// value of this property is `nil`. + var runtimeValue: String? + + /// The fully-qualified name of the type of value represented by + /// `runtimeValue`, or `nil` if that value has not been captured. + var runtimeTypeName: String? + + /// Any child expressions within this expression. + var children: [EncodedExpression]? + + init(encoding expression: borrowing __Expression, in eventContext: borrowing Event.Context) { + sourceCode = expression.sourceCode + runtimeValue = expression.runtimeValue.map(String.init(describingForTest:)) + runtimeTypeName = expression.runtimeValue.map(\.typeInfo.fullyQualifiedName) + if !expression.subexpressions.isEmpty { + children = expression.subexpressions.map { [eventContext = copy eventContext] subexpression in + Self(encoding: subexpression, in: eventContext) + } + } + } + } +} + +// MARK: - Codable + +extension ABIv0.EncodedExpression: Codable {} diff --git a/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedIssue.swift b/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedIssue.swift index 2bf1c8462..47e3bd066 100644 --- a/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedIssue.swift +++ b/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedIssue.swift @@ -32,6 +32,11 @@ extension ABIv0 { /// - Warning: Errors are not yet part of the JSON schema. var _error: EncodedError? + /// The expectation associated with this issue, if applicable. + /// + /// - Warning: Expectations are not yet part of the JSON schema. + var _expectation: EncodedExpectation? + init(encoding issue: borrowing Issue, in eventContext: borrowing Event.Context) { isKnown = issue.isKnown sourceLocation = issue.sourceLocation @@ -41,6 +46,9 @@ extension ABIv0 { if let error = issue.error { _error = EncodedError(encoding: error, in: eventContext) } + if case let .expectationFailed(expectation) = issue.kind { + _expectation = EncodedExpectation(encoding: expectation, in: eventContext) + } } } } diff --git a/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedMessage.swift b/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedMessage.swift index 5cfbf647c..828c1d7a4 100644 --- a/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedMessage.swift +++ b/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedMessage.swift @@ -61,11 +61,19 @@ extension ABIv0 { /// The symbol associated with this message. var symbol: Symbol + /// How much to indent this message when presenting it. + /// + /// - Warning: This property is not yet part of the JSON schema. + var _indentation: Int? + /// The human-readable, unformatted text associated with this message. var text: String init(encoding message: borrowing Event.HumanReadableOutputRecorder.Message) { symbol = Symbol(encoding: message.symbol ?? .default) + if message.indentation > 0 { + _indentation = message.indentation + } text = message.conciseStringValue ?? message.stringValue } } diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index 16a173b4b..a86c8a88e 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -17,6 +17,8 @@ add_library(Testing ABI/v0/Encoded/ABIv0.EncodedBacktrace.swift ABI/v0/Encoded/ABIv0.EncodedError.swift ABI/v0/Encoded/ABIv0.EncodedEvent.swift + ABI/v0/Encoded/ABIv0.EncodedExpectation.swift + ABI/v0/Encoded/ABIv0.EncodedExpression.swift ABI/v0/Encoded/ABIv0.EncodedInstant.swift ABI/v0/Encoded/ABIv0.EncodedIssue.swift ABI/v0/Encoded/ABIv0.EncodedMessage.swift @@ -39,6 +41,8 @@ add_library(Testing Expectations/Expectation.swift Expectations/Expectation+Macro.swift Expectations/ExpectationChecking+Macro.swift + Expectations/ExpectationContext.swift + Expectations/ExpectationContext+Pointers.swift Issues/Confirmation.swift Issues/ErrorSnapshot.swift Issues/Issue.swift @@ -61,7 +65,7 @@ add_library(Testing SourceAttribution/Backtrace+Symbolication.swift SourceAttribution/CustomTestStringConvertible.swift SourceAttribution/Expression.swift - SourceAttribution/Expression+Macro.swift + SourceAttribution/ExpressionID.swift SourceAttribution/SourceContext.swift SourceAttribution/SourceLocation.swift SourceAttribution/SourceLocation+Macro.swift diff --git a/Sources/Testing/Events/Recorder/Event.ConsoleOutputRecorder.swift b/Sources/Testing/Events/Recorder/Event.ConsoleOutputRecorder.swift index b1e90c535..a2332402d 100644 --- a/Sources/Testing/Events/Recorder/Event.ConsoleOutputRecorder.swift +++ b/Sources/Testing/Events/Recorder/Event.ConsoleOutputRecorder.swift @@ -129,6 +129,22 @@ private let _ansiEscapeCodePrefix = "\u{001B}[" private let _resetANSIEscapeCode = "\(_ansiEscapeCodePrefix)0m" extension Event.Symbol { + /// Get the string value to use for a message with no associated symbol. + /// + /// - Parameters: + /// - options: Options to use when writing the symbol. + /// + /// - Returns: A string representation of "no symbol" appropriate for writing + /// to a stream. + fileprivate static func placeholderStringValue(options: Event.ConsoleOutputRecorder.Options) -> String { +#if os(macOS) || (os(iOS) && targetEnvironment(macCatalyst)) + if options.useSFSymbols { + return " " + } +#endif + return " " + } + /// Get the string value for this symbol with the given write options. /// /// - Parameters: @@ -169,7 +185,7 @@ extension Event.Symbol { case .attachment: return "\(_ansiEscapeCodePrefix)94m\(symbolCharacter)\(_resetANSIEscapeCode)" case .details: - return symbolCharacter + return "\(symbolCharacter)" } } return "\(symbolCharacter)" @@ -303,18 +319,12 @@ extension Event.ConsoleOutputRecorder { /// - Returns: Whether any output was produced and written to this instance's /// destination. @discardableResult public func record(_ event: borrowing Event, in context: borrowing Event.Context) -> Bool { - let messages = _humanReadableOutputRecorder.record(event, in: context) - - // Padding to use in place of a symbol for messages that don't have one. - var padding = " " -#if os(macOS) || (os(iOS) && targetEnvironment(macCatalyst)) - if options.useSFSymbols { - padding = " " - } -#endif + let symbolPlaceholder = Event.Symbol.placeholderStringValue(options: options) + let messages = _humanReadableOutputRecorder.record(event, in: context) let lines = messages.lazy.map { [test = context.test] message in - let symbol = message.symbol?.stringValue(options: options) ?? padding + let symbol = message.symbol?.stringValue(options: options) ?? symbolPlaceholder + let indentation = String(repeating: " ", count: message.indentation) if case .details = message.symbol { // Special-case the detail symbol to apply grey to the entire line of @@ -323,17 +333,17 @@ extension Event.ConsoleOutputRecorder { // to the indentation provided by the symbol. var lines = message.stringValue.split(whereSeparator: \.isNewline) lines = CollectionOfOne(lines[0]) + lines.dropFirst().map { line in - "\(padding) \(line)" + "\(indentation)\(symbolPlaceholder) \(line)" } let stringValue = lines.joined(separator: "\n") if options.useANSIEscapeCodes, options.ansiColorBitDepth > 1 { - return "\(_ansiEscapeCodePrefix)90m\(symbol) \(stringValue)\(_resetANSIEscapeCode)\n" + return "\(_ansiEscapeCodePrefix)90m\(symbol) \(indentation)\(stringValue)\(_resetANSIEscapeCode)\n" } else { - return "\(symbol) \(stringValue)\n" + return "\(symbol) \(indentation)\(stringValue)\n" } } else { let colorDots = test.map { self.colorDots(for: $0.tags) } ?? "" - return "\(symbol) \(colorDots)\(message.stringValue)\n" + return "\(symbol) \(indentation)\(colorDots)\(message.stringValue)\n" } } diff --git a/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift b/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift index 98303f11c..91ebd96d6 100644 --- a/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift +++ b/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift @@ -25,6 +25,15 @@ extension Event { /// The symbol associated with this message, if any. var symbol: Symbol? + /// How much to indent this message when presenting it. + /// + /// The way in which this additional indentation is rendered is + /// implementation-defined. Typically, the greater the value of this + /// property, the more whitespace characters are inserted. + /// + /// Rendering of indentation is optional. + var indentation = 0 + /// The human-readable message. var stringValue: String @@ -415,20 +424,18 @@ extension Event.HumanReadableOutputRecorder { } additionalMessages += _formattedComments(issue.comments) - if verbosity > 0, case let .expectationFailed(expectation) = issue.kind { + if verbosity >= 0, case let .expectationFailed(expectation) = issue.kind { let expression = expectation.evaluatedExpression - func addMessage(about expression: __Expression) { - let description = expression.expandedDebugDescription() - additionalMessages.append(Message(symbol: .details, stringValue: description)) - } - let subexpressions = expression.subexpressions - if subexpressions.isEmpty { - addMessage(about: expression) - } else { - for subexpression in subexpressions { - addMessage(about: subexpression) + func addMessage(about expression: __Expression, depth: Int) { + let description = expression.expandedDescription(verbose: verbosity > 0) + if description != expression.sourceCode { + additionalMessages.append(Message(symbol: .details, indentation: depth, stringValue: description)) + } + for subexpression in expression.subexpressions { + addMessage(about: subexpression, depth: depth + 1) } } + addMessage(about: expression, depth: 0) } let atSourceLocation = issue.sourceLocation.map { " at \($0)" } ?? "" diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index f6ea88d22..e999d0449 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -283,7 +283,7 @@ func callExitTest( identifiedBy exitTestID: ExitTest.ID, exitsWith expectedExitCondition: ExitCondition, observing observedValues: [any PartialKeyPath & Sendable], - expression: __Expression, + sourceCode: @escaping @autoclosure @Sendable () -> [__ExpressionID: String], comments: @autoclosure () -> [Comment], isRequired: Bool, isolation: isolated (any Actor)? = #isolation, @@ -335,10 +335,14 @@ func callExitTest( let actualExitCondition = result.exitCondition // Plumb the exit test's result through the general expectation machinery. - return __checkValue( + let expectationContext = __ExpectationContext( + sourceCode: sourceCode(), + runtimeValues: [.root: { Expression.Value(reflecting: actualExitCondition) }] + ) + return check( expectedExitCondition == actualExitCondition, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(actualExitCondition), + expectationContext: expectationContext, + mismatchedErrorDescription: nil, mismatchedExitConditionDescription: String(describingForTest: expectedExitCondition), comments: comments(), isRequired: isRequired, diff --git a/Sources/Testing/Expectations/Expectation+Macro.swift b/Sources/Testing/Expectations/Expectation+Macro.swift index 7e82b2144..906f85672 100644 --- a/Sources/Testing/Expectations/Expectation+Macro.swift +++ b/Sources/Testing/Expectations/Expectation+Macro.swift @@ -66,10 +66,10 @@ /// running in the current task and an instance of ``ExpectationFailedError`` is /// thrown. @freestanding(expression) public macro require( - _ optionalValue: T?, + _ optionalValue: consuming T?, _ comment: @autoclosure () -> Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation -) -> T = #externalMacro(module: "TestingMacros", type: "RequireMacro") +) -> T = #externalMacro(module: "TestingMacros", type: "UnwrapMacro") where T: ~Copyable /// Unwrap an optional boolean value or, if it is `nil`, fail and throw an /// error. @@ -124,10 +124,10 @@ public macro require( @freestanding(expression) @_documentation(visibility: private) public macro require( - _ optionalValue: T, + _ optionalValue: consuming T, _ comment: @autoclosure () -> Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation -) -> T = #externalMacro(module: "TestingMacros", type: "NonOptionalRequireMacro") +) -> T = #externalMacro(module: "TestingMacros", type: "NonOptionalRequireMacro") where T: ~Copyable // MARK: - Matching errors by type diff --git a/Sources/Testing/Expectations/Expectation.swift b/Sources/Testing/Expectations/Expectation.swift index 3254544b6..8888bf367 100644 --- a/Sources/Testing/Expectations/Expectation.swift +++ b/Sources/Testing/Expectations/Expectation.swift @@ -12,14 +12,14 @@ public struct Expectation: Sendable { /// The expression evaluated by this expectation. @_spi(ForToolsIntegrationOnly) - public var evaluatedExpression: Expression + public internal(set) var evaluatedExpression: Expression /// A description of the error mismatch that occurred, if any. /// /// If this expectation passed, the value of this property is `nil` because no /// error mismatch occurred. @_spi(Experimental) @_spi(ForToolsIntegrationOnly) - public var mismatchedErrorDescription: String? + public internal(set) var mismatchedErrorDescription: String? /// A description of the difference between the operands in the expression /// evaluated by this expectation, if the difference could be determined. @@ -28,7 +28,9 @@ public struct Expectation: Sendable { /// the difference is only computed when necessary to assist with diagnosing /// test failures. @_spi(Experimental) @_spi(ForToolsIntegrationOnly) - public var differenceDescription: String? + public var differenceDescription: String? { + evaluatedExpression.differenceDescription + } /// A description of the exit condition that was expected to be matched. /// diff --git a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift index 267fddba4..5ec7a17b3 100644 --- a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift +++ b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift @@ -13,13 +13,12 @@ /// /// - Parameters: /// - condition: The condition to be evaluated. -/// - expression: The expression, corresponding to `condition`, that is being -/// evaluated (if available at compile time.) -/// - expressionWithCapturedRuntimeValues: The expression, corresponding to -/// `condition` and with runtime values captured, that is being evaluated -/// (if available at compile time.) -/// - difference: The difference between the operands in `condition`, if -/// available. Most callers should pass `nil`. +/// - expectationContext: The expectation context, created by the caller, that +/// contains information about `condition` and its subexpressions (if any.) +/// - mismatchedErrorDescription: A description of the thrown error that did +/// not match the expectation, if applicable. +/// - mismatchedExitConditionDescription: A description of the exit condition +/// of the child process that did not match the expectation, if applicable. /// - comments: An array of comments describing the expectation. This array /// may be empty. /// - isRequired: Whether or not the expectation is required. The value of @@ -55,47 +54,26 @@ /// By _returning_ the error this function "throws", we can customize whether or /// not we throw that error during macro resolution without affecting any errors /// thrown by the condition expression passed to it. -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkValue( +func check( _ condition: Bool, - expression: __Expression, - expressionWithCapturedRuntimeValues: @autoclosure () -> __Expression? = nil, - mismatchedErrorDescription: @autoclosure () -> String? = nil, - difference: @autoclosure () -> String? = nil, - mismatchedExitConditionDescription: @autoclosure () -> String? = nil, + expectationContext: consuming __ExpectationContext, + mismatchedErrorDescription: @autoclosure () -> String?, + mismatchedExitConditionDescription: @autoclosure () -> String?, comments: @autoclosure () -> [Comment], isRequired: Bool, sourceLocation: SourceLocation ) -> Result { - // If the expression being evaluated is a negation (!x instead of x), flip - // the condition here so that we evaluate it in the correct sense. We loop - // in case of multiple prefix operators (!!(a == b), for example.) - var condition = condition - do { - var expression: __Expression? = expression - while case let .negation(subexpression, _) = expression?.kind { - defer { - expression = subexpression - } - condition = !condition - } - } - - // Capture the correct expression in the expectation. - var expression = expression - if !condition, let expressionWithCapturedRuntimeValues = expressionWithCapturedRuntimeValues() { - expression = expressionWithCapturedRuntimeValues - if expression.runtimeValue == nil, case .negation = expression.kind { - expression = expression.capturingRuntimeValue(condition) - } - } + let expectationContext = consume expectationContext // Post an event for the expectation regardless of whether or not it passed. // If the current event handler is not configured to handle events of this // kind, this event is discarded. - lazy var expectation = Expectation(evaluatedExpression: expression, isPassing: condition, isRequired: isRequired, sourceLocation: sourceLocation) + lazy var expectation = Expectation( + evaluatedExpression: expectationContext.finalize(successfully: condition), + isPassing: condition, + isRequired: isRequired, + sourceLocation: sourceLocation + ) if Configuration.deliverExpectationCheckedEvents { Event.post(.expectationChecked(expectation)) } @@ -108,7 +86,6 @@ public func __checkValue( // Since this expectation failed, populate its optional fields which are // only evaluated and included lazily upon failure. expectation.mismatchedErrorDescription = mismatchedErrorDescription() - expectation.differenceDescription = difference() expectation.mismatchedExitConditionDescription = mismatchedExitConditionDescription() // Ensure the backtrace is captured here so it has fewer extraneous frames @@ -120,702 +97,190 @@ public func __checkValue( return .failure(ExpectationFailedError(expectation: expectation)) } -// MARK: - Binary operators - -/// Call a binary operator, passing the left-hand and right-hand arguments. -/// -/// - Parameters: -/// - lhs: The left-hand argument to `op`. -/// - op: The binary operator to call. -/// - rhs: The right-hand argument to `op`. This closure may be invoked zero -/// or one time, but not twice or more. -/// -/// - Returns: A tuple containing the result of calling `op` and the value of -/// `rhs` (or `nil` if it was not evaluated.) -/// -/// - Throws: Whatever is thrown by `op`. -private func _callBinaryOperator( - _ lhs: T, - _ op: (T, () -> U) -> R, - _ rhs: () -> U -) -> (result: R, rhs: U?) { - // The compiler normally doesn't allow a nonescaping closure to call another - // nonescaping closure, but our use cases are safe (e.g. `true && false`) and - // we cannot force one function or the other to be escaping. Use - // withoutActuallyEscaping() to tell the compiler that what we're doing is - // okay. SEE: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0176-enforce-exclusive-access-to-memory.md#restrictions-on-recursive-uses-of-non-escaping-closures - var rhsValue: U? - let result: R = withoutActuallyEscaping(rhs) { rhs in - op(lhs, { - if rhsValue == nil { - rhsValue = rhs() - } - return rhsValue! - }) - } - return (result, rhsValue) -} +// MARK: - Expectation checks -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload is used by binary operators such as `>`: -/// -/// ```swift -/// #expect(2 > 1) -/// ``` +/// A function that evaluates some boolean condition value on behalf of +/// `#expect()` or `#require()`. /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -@_disfavoredOverload public func __checkBinaryOperation( - _ lhs: T, _ op: (T, () -> U) -> Bool, _ rhs: @autoclosure () -> U, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) -> Result { - let (condition, rhs) = _callBinaryOperator(lhs, op, rhs) - return __checkValue( - condition, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, rhs), - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) -} - -// MARK: - Function calls - -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload is used by function calls: -/// -/// ```swift -/// #expect(x.update(i)) -/// ``` -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkFunctionCall( - _ lhs: T, calling functionCall: (T, repeat each U) throws -> Bool, _ arguments: repeat each U, - expression: __Expression, +public func __checkCondition( + _ condition: (inout __ExpectationContext) throws -> Bool, + sourceCode: @escaping @autoclosure @Sendable () -> [__ExpressionID: String], comments: @autoclosure () -> [Comment], isRequired: Bool, sourceLocation: SourceLocation ) rethrows -> Result { - let condition = try functionCall(lhs, repeat each arguments) - return __checkValue( - condition, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, repeat each arguments), - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) -} + var expectationContext = __ExpectationContext.init(sourceCode: sourceCode()) + let condition = try condition(&expectationContext) -#if !SWT_FIXED_122011759 -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload works around a bug in variadic generics that may cause a -/// miscompile when an argument to a function is a C string converted from a -/// Swift string (e.g. the arguments to `fopen("/file/path", "wb")`.) -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkFunctionCall( - _ lhs: T, calling functionCall: (T, Arg0) throws -> Bool, _ argument0: Arg0, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) rethrows -> Result { - let condition = try functionCall(lhs, argument0) - return __checkValue( + return check( condition, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, argument0), + expectationContext: expectationContext, + mismatchedErrorDescription: nil, + mismatchedExitConditionDescription: nil, comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation ) } -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload works around a bug in variadic generics that may cause a -/// miscompile when an argument to a function is a C string converted from a -/// Swift string (e.g. the arguments to `fopen("/file/path", "wb")`.) +/// A function that evaluates some optional condition value on behalf of +/// `#expect()` or `#require()`. /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkFunctionCall( - _ lhs: T, calling functionCall: (T, Arg0, Arg1) throws -> Bool, _ argument0: Arg0, _ argument1: Arg1, - expression: __Expression, +public func __checkCondition( + _ optionalValue: (inout __ExpectationContext) throws -> T?, + sourceCode: @escaping @autoclosure @Sendable () -> [__ExpressionID: String], comments: @autoclosure () -> [Comment], isRequired: Bool, sourceLocation: SourceLocation -) rethrows -> Result { - let condition = try functionCall(lhs, argument0, argument1) - return __checkValue( - condition, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, argument0, argument1), - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) -} +) rethrows -> Result where T: ~Copyable { + var expectationContext = __ExpectationContext(sourceCode: sourceCode()) + let optionalValue = try optionalValue(&expectationContext) -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload works around a bug in variadic generics that may cause a -/// miscompile when an argument to a function is a C string converted from a -/// Swift string (e.g. the arguments to `fopen("/file/path", "wb")`.) -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkFunctionCall( - _ lhs: T, calling functionCall: (T, Arg0, Arg1, Arg2) throws -> Bool, _ argument0: Arg0, _ argument1: Arg1, _ argument2: Arg2, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) rethrows -> Result { - let condition = try functionCall(lhs, argument0, argument1, argument2) - return __checkValue( - condition, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, argument0, argument1, argument2), - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) -} - -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload works around a bug in variadic generics that may cause a -/// miscompile when an argument to a function is a C string converted from a -/// Swift string (e.g. the arguments to `fopen("/file/path", "wb")`.) -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkFunctionCall( - _ lhs: T, calling functionCall: (T, Arg0, Arg1, Arg2, Arg3) throws -> Bool, _ argument0: Arg0, _ argument1: Arg1, _ argument2: Arg2, _ argument3: Arg3, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) rethrows -> Result { - let condition = try functionCall(lhs, argument0, argument1, argument2, argument3) - return __checkValue( - condition, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, argument0, argument1, argument2, argument3), - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) -} -#endif - -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload is used by function calls where the arguments are all `inout`: -/// -/// ```swift -/// #expect(x.update(&i)) -/// ``` -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkInoutFunctionCall( - _ lhs: T, calling functionCall: (T, inout /*repeat each*/ U) throws -> Bool, _ arguments: inout /*repeat each*/ U, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) rethrows -> Result { - let condition = try functionCall(lhs, /*repeat each*/ &arguments) - return __checkValue( - condition, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, /*repeat each*/ arguments), - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) -} - -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload is used to conditionally unwrap optional values produced from -/// expanded function calls: -/// -/// ```swift -/// let z = try #require(x.update(i)) -/// ``` -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkFunctionCall( - _ lhs: T, calling functionCall: (T, repeat each U) throws -> R?, _ arguments: repeat each U, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) rethrows -> Result { - let optionalValue = try functionCall(lhs, repeat each arguments) - return __checkValue( - optionalValue, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, repeat each arguments), - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) -} - -#if !SWT_FIXED_122011759 -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload works around a bug in variadic generics that may cause a -/// miscompile when an argument to a function is a C string converted from a -/// Swift string (e.g. the arguments to `fopen("/file/path", "wb")`.) -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkFunctionCall( - _ lhs: T, calling functionCall: (T, Arg0) throws -> R?, _ argument0: Arg0, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) rethrows -> Result { - let optionalValue = try functionCall(lhs, argument0) - return __checkValue( - optionalValue, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, argument0), - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) -} - -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload works around a bug in variadic generics that may cause a -/// miscompile when an argument to a function is a C string converted from a -/// Swift string (e.g. the arguments to `fopen("/file/path", "wb")`.) -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkFunctionCall( - _ lhs: T, calling functionCall: (T, Arg0, Arg1) throws -> R?, _ argument0: Arg0, _ argument1: Arg1, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) rethrows -> Result { - let optionalValue = try functionCall(lhs, argument0, argument1) - return __checkValue( - optionalValue, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, argument0, argument1), + let result = check( + optionalValue != nil, + expectationContext: expectationContext, + mismatchedErrorDescription: nil, + mismatchedExitConditionDescription: nil, comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation ) -} -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload works around a bug in variadic generics that may cause a -/// miscompile when an argument to a function is a C string converted from a -/// Swift string (e.g. the arguments to `fopen("/file/path", "wb")`.) -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkFunctionCall( - _ lhs: T, calling functionCall: (T, Arg0, Arg1, Arg2) throws -> R?, _ argument0: Arg0, _ argument1: Arg1, _ argument2: Arg2, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) rethrows -> Result { - let optionalValue = try functionCall(lhs, argument0, argument1, argument2) - return __checkValue( - optionalValue, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, argument0, argument1, argument2), - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) + switch result { + case .success: + return .success(optionalValue!) + case let .failure(error): + return .failure(error) + } } -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload works around a bug in variadic generics that may cause a -/// miscompile when an argument to a function is a C string converted from a -/// Swift string (e.g. the arguments to `fopen("/file/path", "wb")`.) -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkFunctionCall( - _ lhs: T, calling functionCall: (T, Arg0, Arg1, Arg2, Arg3) throws -> R?, _ argument0: Arg0, _ argument1: Arg1, _ argument2: Arg2, _ argument3: Arg3, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) rethrows -> Result { - let optionalValue = try functionCall(lhs, argument0, argument1, argument2, argument3) - return __checkValue( - optionalValue, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, argument0, argument1, argument2, argument3), - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) -} -#endif +// MARK: - Asynchronous expectation checks -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload is used to conditionally unwrap optional values produced from -/// expanded function calls where the arguments are all `inout`: -/// -/// ```swift -/// let z = try #require(x.update(&i)) -/// ``` +/// A function that evaluates some asynchronous boolean condition value on +/// behalf of `#expect()` or `#require()`. /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkInoutFunctionCall( - _ lhs: T, calling functionCall: (T, inout /*repeat each*/ U) throws -> R?, _ arguments: inout /*repeat each*/ U, - expression: __Expression, +public func __checkConditionAsync( + _ condition: (inout __ExpectationContext) async throws -> Bool, + sourceCode: @escaping @autoclosure @Sendable () -> [__ExpressionID: String], comments: @autoclosure () -> [Comment], isRequired: Bool, + isolation: isolated (any Actor)? = #isolation, sourceLocation: SourceLocation -) rethrows -> Result { - let optionalValue = try functionCall(lhs, /*repeat each*/ &arguments) - return __checkValue( - optionalValue, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, /*repeat each*/ arguments), - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) -} +) async rethrows -> Result { + var expectationContext = __ExpectationContext(sourceCode: sourceCode()) + let condition = try await condition(&expectationContext) -// MARK: - Property access - -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload is used by property accesses: -/// -/// ```swift -/// #expect(x.isFoodTruck) -/// ``` -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkPropertyAccess( - _ lhs: T, getting memberAccess: (T) -> Bool, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) -> Result { - let condition = memberAccess(lhs) - return __checkValue( + return check( condition, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, condition), + expectationContext: expectationContext, + mismatchedErrorDescription: nil, + mismatchedExitConditionDescription: nil, comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation ) } -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload is used to conditionally unwrap optional values produced from -/// expanded property accesses: -/// -/// ```swift -/// let z = try #require(x.nearestFoodTruck) -/// ``` +/// A function that evaluates some asynchronous optional condition value on +/// behalf of `#expect()` or `#require()`. /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkPropertyAccess( - _ lhs: T, getting memberAccess: (T) -> U?, - expression: __Expression, +public func __checkConditionAsync( + _ optionalValue: (inout __ExpectationContext) async throws -> sending T?, + sourceCode: @escaping @autoclosure @Sendable () -> [__ExpressionID: String], comments: @autoclosure () -> [Comment], isRequired: Bool, + isolation: isolated (any Actor)? = #isolation, sourceLocation: SourceLocation -) -> Result { - let optionalValue = memberAccess(lhs) - return __checkValue( - optionalValue, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, optionalValue as U??), +) async rethrows -> Result where T: ~Copyable { + var expectationContext = __ExpectationContext(sourceCode: sourceCode()) + let optionalValue = try await optionalValue(&expectationContext) + + let result = check( + optionalValue != nil, + expectationContext: expectationContext, + mismatchedErrorDescription: nil, + mismatchedExitConditionDescription: nil, comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation ) -} - -// MARK: - Collection diffing -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload is used to implement difference-reporting support when -/// comparing collections. -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -@_disfavoredOverload public func __checkBinaryOperation( - _ lhs: T, _ op: (T, () -> T) -> Bool, _ rhs: @autoclosure () -> T, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) -> Result where T: BidirectionalCollection, T.Element: Equatable { - let (condition, rhs) = _callBinaryOperator(lhs, op, rhs) - func difference() -> String? { - guard let rhs else { - return nil - } - let difference = lhs.difference(from: rhs) - let insertions = difference.insertions.map(\.element) - let removals = difference.removals.map(\.element) - switch (!insertions.isEmpty, !removals.isEmpty) { - case (true, true): - return "inserted \(insertions), removed \(removals)" - case (true, false): - return "inserted \(insertions)" - case (false, true): - return "removed \(removals)" - case (false, false): - return "" - } + switch result { + case .success: + return .success(optionalValue!) + case let .failure(error): + return .failure(error) } - - return __checkValue( - condition, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, rhs), - difference: difference(), - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) } -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload is necessary because `String` satisfies the requirements for -/// the difference-calculating overload above, but the output from that overload -/// may be unexpectedly complex. +// MARK: - "Escape hatch" expectation checks + +/// The "escape hatch" overload of `__check()` that is used when a developer has +/// opted out of most of the magic of macro expansion (presumably due to a +/// miscompile.) /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkBinaryOperation( - _ lhs: String, _ op: (String, () -> String) -> Bool, _ rhs: @autoclosure () -> String, - expression: __Expression, +public func __checkEscapedCondition( + _ condition: Bool, + sourceCode: @escaping @autoclosure @Sendable () -> [__ExpressionID: String], comments: @autoclosure () -> [Comment], isRequired: Bool, sourceLocation: SourceLocation ) -> Result { - let (condition, rhs) = _callBinaryOperator(lhs, op, rhs) - return __checkValue( - condition, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, rhs), - difference: nil, - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) -} + let expectationContext = __ExpectationContext(sourceCode: sourceCode()) -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload is necessary because ranges are collections and satisfy the -/// requirements for the difference-calculating overload above, but it doesn't -/// make sense to diff them and very large ranges can cause overflows or hangs. -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkBinaryOperation( - _ lhs: T, _ op: (T, () -> U) -> Bool, _ rhs: @autoclosure () -> U, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) -> Result where T: RangeExpression, U: RangeExpression { - let (condition, rhs) = _callBinaryOperator(lhs, op, rhs) - return __checkValue( + return check( condition, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, rhs), - difference: nil, + expectationContext: expectationContext, + mismatchedErrorDescription: nil, + mismatchedExitConditionDescription: nil, comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation ) } -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. +/// The "escape hatch" overload of `__check()` that is used when a developer has +/// opted out of most of the magic of macro expansion (presumably due to a +/// miscompile.) /// -/// This overload is used for `v is T` expressions. +/// This overload is used when the expectation is unwrapping an optional value. /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkCast( - _ value: V, - is _: T.Type, - expression: __Expression, +public func __checkEscapedCondition( + _ optionalValue: consuming T?, + sourceCode: @escaping @autoclosure @Sendable () -> [__ExpressionID: String], comments: @autoclosure () -> [Comment], isRequired: Bool, sourceLocation: SourceLocation -) -> Result { - return __checkValue( - value is T, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(value, type(of: value as Any)), - difference: nil, - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ) -} - -// MARK: - Optional unwrapping +) -> Result where T: ~Copyable { + let expectationContext = __ExpectationContext(sourceCode: sourceCode()) -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload is used to conditionally unwrap optional values: -/// -/// ```swift -/// let x: Int? = ... -/// let y = try #require(x) -/// ``` -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkValue( - _ optionalValue: T?, - expression: __Expression, - expressionWithCapturedRuntimeValues: @autoclosure () -> __Expression? = nil, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) -> Result { - // The double-optional below is because capturingRuntimeValue() takes optional - // values and interprets nil as "no value available". Rather, if optionalValue - // is `nil`, we want to actually store `nil` as the expression's evaluated - // value. The outer optional satisfies the generic constraint of - // capturingRuntimeValue(), and the inner optional represents the actual value - // (`nil`) that will be captured. - __checkValue( + let result = check( optionalValue != nil, - expression: expression, - expressionWithCapturedRuntimeValues: (expressionWithCapturedRuntimeValues() ?? expression).capturingRuntimeValue(optionalValue as T??), - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ).map { - optionalValue.unsafelyUnwrapped - } -} - -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload is used to conditionally unwrap optional values using the `??` -/// operator: -/// -/// ```swift -/// let x: Int? = ... -/// let y: Int? = ... -/// let z = try #require(x ?? y) -/// ``` -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -@_disfavoredOverload public func __checkBinaryOperation( - _ lhs: T?, _ op: (T?, () -> T?) -> T?, _ rhs: @autoclosure () -> T?, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) -> Result { - let (optionalValue, rhs) = _callBinaryOperator(lhs, op, rhs) - return __checkValue( - optionalValue, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs as T??, rhs as T??), + expectationContext: expectationContext, + mismatchedErrorDescription: nil, + mismatchedExitConditionDescription: nil, comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation ) -} -/// Check that an expectation has passed after a condition has been evaluated -/// and throw an error if it failed. -/// -/// This overload is used for `v as? T` expressions. -/// -/// - Warning: This function is used to implement the `#expect()` and -/// `#require()` macros. Do not call it directly. -public func __checkCast( - _ value: V, - as _: T.Type, - expression: __Expression, - comments: @autoclosure () -> [Comment], - isRequired: Bool, - sourceLocation: SourceLocation -) -> Result { - // NOTE: this call to __checkValue() does not go through the optional - // bottleneck because we do not want to capture the nil value on failure (it - // looks odd in test output.) - let optionalValue = value as? T - return __checkValue( - optionalValue != nil, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(value, type(of: value as Any)), - comments: comments(), - isRequired: isRequired, - sourceLocation: sourceLocation - ).map { - optionalValue.unsafelyUnwrapped + switch result { + case .success: + return .success(optionalValue!) + case let .failure(error): + return .failure(error) } } @@ -832,7 +297,7 @@ public func __checkCast( public func __checkClosureCall( throws errorType: E.Type, performing body: () throws -> some Any, - expression: __Expression, + sourceCode: @escaping @autoclosure @Sendable () -> [__ExpressionID: String], comments: @autoclosure () -> [Comment], isRequired: Bool, sourceLocation: SourceLocation @@ -841,7 +306,7 @@ public func __checkClosureCall( __checkClosureCall( throws: Never.self, performing: body, - expression: expression, + sourceCode: sourceCode(), comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation @@ -851,7 +316,7 @@ public func __checkClosureCall( performing: body, throws: { $0 is E }, mismatchExplanation: { "expected error of type \(errorType), but \(_description(of: $0)) was thrown instead" }, - expression: expression, + sourceCode: sourceCode(), comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation @@ -870,7 +335,7 @@ public func __checkClosureCall( public func __checkClosureCall( throws errorType: E.Type, performing body: () async throws -> sending some Any, - expression: __Expression, + sourceCode: @escaping @autoclosure @Sendable () -> [__ExpressionID: String], comments: @autoclosure () -> [Comment], isRequired: Bool, isolation: isolated (any Actor)? = #isolation, @@ -880,7 +345,7 @@ public func __checkClosureCall( await __checkClosureCall( throws: Never.self, performing: body, - expression: expression, + sourceCode: sourceCode(), comments: comments(), isRequired: isRequired, isolation: isolation, @@ -891,7 +356,7 @@ public func __checkClosureCall( performing: body, throws: { $0 is E }, mismatchExplanation: { "expected error of type \(errorType), but \(_description(of: $0)) was thrown instead" }, - expression: expression, + sourceCode: sourceCode(), comments: comments(), isRequired: isRequired, isolation: isolation, @@ -913,7 +378,7 @@ public func __checkClosureCall( public func __checkClosureCall( throws _: Never.Type, performing body: () throws -> some Any, - expression: __Expression, + sourceCode: @escaping @autoclosure @Sendable () -> [__ExpressionID: String], comments: @autoclosure () -> [Comment], isRequired: Bool, sourceLocation: SourceLocation @@ -927,10 +392,12 @@ public func __checkClosureCall( mismatchExplanationValue = "an error was thrown when none was expected: \(_description(of: error))" } - return __checkValue( + let expectationContext = __ExpectationContext(sourceCode: sourceCode()) + return check( success, - expression: expression, + expectationContext: expectationContext, mismatchedErrorDescription: mismatchExplanationValue, + mismatchedExitConditionDescription: nil, comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation @@ -949,7 +416,7 @@ public func __checkClosureCall( public func __checkClosureCall( throws _: Never.Type, performing body: () async throws -> sending some Any, - expression: __Expression, + sourceCode: @escaping @autoclosure @Sendable () -> [__ExpressionID: String], comments: @autoclosure () -> [Comment], isRequired: Bool, isolation: isolated (any Actor)? = #isolation, @@ -964,10 +431,12 @@ public func __checkClosureCall( mismatchExplanationValue = "an error was thrown when none was expected: \(_description(of: error))" } - return __checkValue( + let expectationContext = __ExpectationContext(sourceCode: sourceCode()) + return check( success, - expression: expression, + expectationContext: expectationContext, mismatchedErrorDescription: mismatchExplanationValue, + mismatchedExitConditionDescription: nil, comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation @@ -986,7 +455,7 @@ public func __checkClosureCall( public func __checkClosureCall( throws error: E, performing body: () throws -> some Any, - expression: __Expression, + sourceCode: @escaping @autoclosure @Sendable () -> [__ExpressionID: String], comments: @autoclosure () -> [Comment], isRequired: Bool, sourceLocation: SourceLocation @@ -995,7 +464,7 @@ public func __checkClosureCall( performing: body, throws: { true == (($0 as? E) == error) }, mismatchExplanation: { "expected error \(_description(of: error)), but \(_description(of: $0)) was thrown instead" }, - expression: expression, + sourceCode: sourceCode(), comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation @@ -1012,7 +481,7 @@ public func __checkClosureCall( public func __checkClosureCall( throws error: E, performing body: () async throws -> sending some Any, - expression: __Expression, + sourceCode: @escaping @autoclosure @Sendable () -> [__ExpressionID: String], comments: @autoclosure () -> [Comment], isRequired: Bool, isolation: isolated (any Actor)? = #isolation, @@ -1022,7 +491,7 @@ public func __checkClosureCall( performing: body, throws: { true == (($0 as? E) == error) }, mismatchExplanation: { "expected error \(_description(of: error)), but \(_description(of: $0)) was thrown instead" }, - expression: expression, + sourceCode: sourceCode(), comments: comments(), isRequired: isRequired, isolation: isolation, @@ -1042,14 +511,15 @@ public func __checkClosureCall( performing body: () throws -> R, throws errorMatcher: (any Error) throws -> Bool, mismatchExplanation: ((any Error) -> String)? = nil, - expression: __Expression, + sourceCode: @escaping @autoclosure @Sendable () -> [__ExpressionID: String], comments: @autoclosure () -> [Comment], isRequired: Bool, sourceLocation: SourceLocation ) -> Result<(any Error)?, any Error> { + var expectationContext = __ExpectationContext(sourceCode: sourceCode()) + var errorMatches = false var mismatchExplanationValue: String? = nil - var expression = expression var caughtError: (any Error)? do { let result = try body() @@ -1061,7 +531,7 @@ public func __checkClosureCall( mismatchExplanationValue = explanation } catch { caughtError = error - expression = expression.capturingRuntimeValues(error) + expectationContext.runtimeValues[.root] = { Expression.Value(reflecting: error) } let secondError = Issue.withErrorRecording(at: sourceLocation) { errorMatches = try errorMatcher(error) } @@ -1072,10 +542,11 @@ public func __checkClosureCall( } } - return __checkValue( + return check( errorMatches, - expression: expression, + expectationContext: expectationContext, mismatchedErrorDescription: mismatchExplanationValue, + mismatchedExitConditionDescription: nil, comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation @@ -1092,15 +563,16 @@ public func __checkClosureCall( performing body: () async throws -> sending R, throws errorMatcher: (any Error) async throws -> Bool, mismatchExplanation: ((any Error) -> String)? = nil, - expression: __Expression, + sourceCode: @escaping @autoclosure @Sendable () -> [__ExpressionID: String], comments: @autoclosure () -> [Comment], isRequired: Bool, isolation: isolated (any Actor)? = #isolation, sourceLocation: SourceLocation ) async -> Result<(any Error)?, any Error> { + var expectationContext = __ExpectationContext(sourceCode: sourceCode()) + var errorMatches = false var mismatchExplanationValue: String? = nil - var expression = expression var caughtError: (any Error)? do { let result = try await body() @@ -1112,7 +584,7 @@ public func __checkClosureCall( mismatchExplanationValue = explanation } catch { caughtError = error - expression = expression.capturingRuntimeValues(error) + expectationContext.runtimeValues[.root] = { Expression.Value(reflecting: error) } let secondError = await Issue.withErrorRecording(at: sourceLocation) { errorMatches = try await errorMatcher(error) } @@ -1123,10 +595,11 @@ public func __checkClosureCall( } } - return __checkValue( + return check( errorMatches, - expression: expression, + expectationContext: expectationContext, mismatchedErrorDescription: mismatchExplanationValue, + mismatchedExitConditionDescription: nil, comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation @@ -1151,7 +624,7 @@ public func __checkClosureCall( exitsWith expectedExitCondition: ExitCondition, observing observedValues: [any PartialKeyPath & Sendable], performing body: @convention(thin) () -> Void, - expression: __Expression, + sourceCode: @escaping @autoclosure @Sendable () -> [__ExpressionID: String], comments: @autoclosure () -> [Comment], isRequired: Bool, isolation: isolated (any Actor)? = #isolation, @@ -1161,7 +634,7 @@ public func __checkClosureCall( identifiedBy: exitTestID, exitsWith: expectedExitCondition, observing: observedValues, - expression: expression, + sourceCode: sourceCode(), comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation diff --git a/Sources/Testing/Expectations/ExpectationContext+Pointers.swift b/Sources/Testing/Expectations/ExpectationContext+Pointers.swift new file mode 100644 index 000000000..d6304c379 --- /dev/null +++ b/Sources/Testing/Expectations/ExpectationContext+Pointers.swift @@ -0,0 +1,146 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +#if !SWT_NO_IMPLICIT_POINTER_CASTING +// MARK: String-to-C-string handling and implicit pointer conversions + +extension __ExpectationContext { + /// A protocol describing types that can be implicitly cast to C strings or + /// pointers when passed to C functions. + /// + /// This protocol helps the compiler disambiguate string values when they need + /// to be implicitly cast to C strings or other pointer types. + /// + /// - Warning: This protocol is used to implement the `#expect()` and + /// `#require()` macros. Do not use it directly. Do not add conformances to + /// this protocol outside of the testing library. + public protocol __ImplicitlyPointerConvertible { + /// The concrete type of the resulting pointer when an instance of this type + /// is implicitly cast. + associatedtype __ImplicitPointerConversionResult + + /// Perform an implicit cast of this instance to its corresponding pointer + /// type. + /// + /// - Parameters: + /// - expectationContext: The expectation context that needs to cast this + /// instance. + /// + /// - Returns: A copy of this instance, cast to a pointer. + /// + /// The implementation of this method should register the resulting pointer + /// with `expectationContext` so that it is not leaked. + /// + /// - Warning: This function is used to implement the `#expect()` and + /// `#require()` macros. Do not call it directly. + func __implicitlyCast(for expectationContext: inout __ExpectationContext) -> __ImplicitPointerConversionResult + } + + /// Capture information about a value for use if the expectation currently + /// being evaluated fails. + /// + /// - Parameters: + /// - value: The value to pass through. + /// - id: A value that uniquely identifies the represented expression in the + /// context of the expectation currently being evaluated. + /// + /// - Returns: `value`, cast to a C string. + /// + /// This overload of `callAsFunction(_:_:)` helps the compiler disambiguate + /// string values when they need to be implicitly cast to C strings or other + /// pointer types. + /// + /// - Warning: This function is used to implement the `#expect()` and + /// `#require()` macros. Do not call it directly. + @_disfavoredOverload + @inlinable public mutating func callAsFunction(_ value: S, _ id: __ExpressionID) -> S.__ImplicitPointerConversionResult where S: __ImplicitlyPointerConvertible { + captureValue(value, id).__implicitlyCast(for: &self) + } + + /// Capture information about a value for use if the expectation currently + /// being evaluated fails. + /// + /// - Parameters: + /// - value: The value to pass through. + /// - id: A value that uniquely identifies the represented expression in the + /// context of the expectation currently being evaluated. + /// + /// - Returns: `value`, verbatim. + /// + /// This overload of `callAsFunction(_:_:)` helps the compiler disambiguate + /// string values when they do _not_ need to be implicitly cast to C strings + /// or other pointer types. Without this overload, all instances of conforming + /// types end up being cast to pointers before being compared (etc.), which + /// produces incorrect results. + /// + /// - Warning: This function is used to implement the `#expect()` and + /// `#require()` macros. Do not call it directly. + @inlinable public mutating func callAsFunction(_ value: S, _ id: __ExpressionID) -> S where S: __ImplicitlyPointerConvertible { + captureValue(value, id) + } + + /// Convert some pointer to another pointer type and capture information about + /// it for use if the expectation currently being evaluated fails. + /// + /// - Parameters: + /// - value: The pointer to cast. + /// - id: A value that uniquely identifies the represented expression in the + /// context of the expectation currently being evaluated. + /// + /// - Returns: `value`, cast to another type of pointer. + /// + /// This overload of `callAsFunction(_:_:)` handles the implicit conversions + /// between various pointer types that are normally provided by the compiler. + /// + /// - Warning: This function is used to implement the `#expect()` and + /// `#require()` macros. Do not call it directly. + @inlinable public mutating func callAsFunction(_ value: P1?, _ id: __ExpressionID) -> P2! where P1: _Pointer, P2: _Pointer { + captureValue(value, id).flatMap { value in + P2(bitPattern: Int(bitPattern: value)) + } + } +} + +extension __ExpectationContext.__ImplicitlyPointerConvertible where Self: Collection { + public func __implicitlyCast(for expectationContext: inout __ExpectationContext) -> UnsafeMutablePointer { + // If `count` is 0, Swift may opt not to allocate any storage, and we'll + // crash dereferencing the base address. + let count = Swift.max(1, count) + + // Create a copy of this collection. Note we don't automatically add a null + // character at the end (for C strings) because that could mask bugs in test + // code that should automatically be adding them. + let resultPointer = UnsafeMutableBufferPointer.allocate(capacity: count) + let initializedEnd = resultPointer.initialize(fromContentsOf: self) + + expectationContext.callWhenDeinitializing { + resultPointer[.. UnsafeMutablePointer { + utf8CString.__implicitlyCast(for: &expectationContext) + } +} + +extension Optional: __ExpectationContext.__ImplicitlyPointerConvertible where Wrapped: __ExpectationContext.__ImplicitlyPointerConvertible { + public func __implicitlyCast(for expectationContext: inout __ExpectationContext) -> Wrapped.__ImplicitPointerConversionResult? { + flatMap { $0.__implicitlyCast(for: &expectationContext) } + } +} + +extension Array: __ExpectationContext.__ImplicitlyPointerConvertible {} +extension ContiguousArray: __ExpectationContext.__ImplicitlyPointerConvertible {} +#endif diff --git a/Sources/Testing/Expectations/ExpectationContext.swift b/Sources/Testing/Expectations/ExpectationContext.swift new file mode 100644 index 000000000..a3697ea2a --- /dev/null +++ b/Sources/Testing/Expectations/ExpectationContext.swift @@ -0,0 +1,454 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +/// A type representing the context within a call to the `#expect()` and +/// `#require()` macros. +/// +/// When the compiler expands a call to either of these macros, it creates a +/// local instance of this type that is used to collect information about the +/// various subexpressions of the macro's condition argument. The nature of the +/// collected information is subject to change over time. +/// +/// - Warning: This type is used to implement the `#expect()` and `#require()` +/// macros. Do not use it directly. +public struct __ExpectationContext: ~Copyable { + /// The source code representations of any captured expressions. + /// + /// Unlike the rest of the state in this type, the source code dictionary is + /// entirely available at compile time and only needs to actually be realized + /// if an issue is recorded (or more rarely if passing expectations are + /// reported to the current event handler.) So we can store the dictionary as + /// a closure instead of always paying the cost to allocate and initialize it. + private var _sourceCode: @Sendable () -> [__ExpressionID: String] + + /// The runtime values of any captured expressions. + /// + /// The values in this dictionary are generally gathered at runtime as + /// subexpressions are evaluated. Not all expressions captured at compile time + /// will have runtime values: notably, if an operand to a short-circuiting + /// binary operator like `&&` is not evaluated, the corresponding expression + /// will not be assigned a runtime value. + var runtimeValues: [__ExpressionID: () -> Expression.Value?] + + /// Computed differences between the operands or arguments of expressions. + /// + /// The values in this dictionary are gathered at runtime as subexpressions + /// are evaluated, much like ``runtimeValues``. + var differences: [__ExpressionID: () -> CollectionDifference?] + + /// Cleanup functions for any locally-created resources (such as pointers or + /// C strings.) + /// + /// The closures in this array are called when this instance is deinitialized. + /// The effect of calling them elsewhere is undefined. + private var _cleanup = [() -> Void]() + + /// Register a callback to invoke when this instance is deinitialized. + /// + /// - Parameters: + /// - cleanup: The callback to invoke when `deinit` is called. + mutating func callWhenDeinitializing(_ cleanup: @escaping () -> Void) { + _cleanup.append(cleanup) + } + + init( + sourceCode: @escaping @autoclosure @Sendable () -> [__ExpressionID: String] = [:], + runtimeValues: [__ExpressionID: () -> Expression.Value?] = [:], + differences: [__ExpressionID: () -> CollectionDifference?] = [:] + ) { + _sourceCode = sourceCode + self.runtimeValues = runtimeValues + self.differences = differences + } + + deinit { + for cleanup in _cleanup { + cleanup() + } + } + + /// Collapse the given expression graph into one or more expressions with + /// nested subexpressions. + /// + /// - Parameters: + /// - expressionGraph: The expression graph to collapse. + /// - depth: How deep into the expression graph this call is. The first call + /// has a depth of `0`. + /// + /// - Returns: An array of expressions under the root node of + /// `expressionGraph`. The expression at the root of the graph is not + /// included in the result. + private borrowing func _squashExpressionGraph(_ expressionGraph: Graph, depth: Int) -> [__Expression] { + var result = [__Expression]() + + let childGraphs = expressionGraph.children.sorted { $0.key < $1.key } + for (_, childGraph) in childGraphs { + let subexpressions = _squashExpressionGraph(childGraph, depth: depth + 1) + if var subexpression = childGraph.value { + subexpression.subexpressions += subexpressions + result.append(subexpression) + } else { + // Hoist subexpressions of the child graph as there was no expression + // recorded for it. + result += subexpressions + } + } + + return result + } + + /// Perform whatever final work is needed on this instance in order to produce + /// an instance of `__Expression` corresponding to the condition expression + /// being evaluated. + /// + /// - Parameters: + /// - successfully: Whether or not the expectation is "successful" (i.e. its + /// condition expression evaluates to `true`). If the expectation failed, + /// more diagnostic information is gathered including the runtime values + /// of any subexpressions of the condition expression. + /// + /// - Returns: An expression value representing the condition expression that + /// was evaluated. + /// + /// - Bug: This function should ideally be `consuming`, but because it is used + /// in a `lazy var` declaration, the compiler currently disallows it. + borrowing func finalize(successfully: Bool) -> __Expression { + // Construct a graph containing the source code for all the subexpressions + // we've captured during evaluation. + var expressionGraph = Graph() + for (id, sourceCode) in _sourceCode() { + let keyPath = id.keyPathRepresentation + expressionGraph.insertValue(__Expression(sourceCode), at: keyPath) + } + + // If the expectation failed, insert any captured runtime values into the + // graph alongside the source code. + if !successfully { + for (id, runtimeValue) in runtimeValues { + let keyPath = id.keyPathRepresentation + if var expression = expressionGraph[keyPath], let runtimeValue = runtimeValue() { + expression.runtimeValue = runtimeValue + expressionGraph[keyPath] = expression + } + } + + for (id, difference) in differences { + let keyPath = id.keyPathRepresentation + if var expression = expressionGraph[keyPath], let difference = difference() { + let differenceDescription = Self._description(of: difference) + expression.differenceDescription = differenceDescription + expressionGraph[keyPath] = expression + } + } + } + + // Flatten the expression graph. + var subexpressions = _squashExpressionGraph(expressionGraph, depth: 0) + var expression = if let rootExpression = expressionGraph.value { + // We had a root expression and can add all reported subexpressions to it. + // This should be the common case. + rootExpression + } else if subexpressions.count == 1 { + // We had no root expression, but we did have a single reported + // subexpression that can serve as our root. + subexpressions.removeFirst() + } else { + // We could not distinguish which subexpression should serve as the root + // expression. In practice this case should be treated as a bug. + __Expression(kind: .generic("")) + } + expression.subexpressions += subexpressions + + return expression + } +} + +@available(*, unavailable) +extension __ExpectationContext: Sendable {} + +// MARK: - Expression capturing + +extension __ExpectationContext { + /// Capture information about a value for use if the expectation currently + /// being evaluated fails. + /// + /// - Parameters: + /// - value: The value to pass through. + /// - id: A value that uniquely identifies the represented expression in the + /// context of the expectation currently being evaluated. + /// + /// - Returns: `value`, verbatim. + /// + /// This function helps overloads of `callAsFunction(_:_:)` disambiguate + /// themselves and avoid accidental recursion. + @usableFromInline mutating func captureValue(_ value: T, _ id: __ExpressionID) -> T { + runtimeValues[id] = { Expression.Value(reflecting: value) } + return value + } + + /// Capture information about a value for use if the expectation currently + /// being evaluated fails. + /// + /// - Parameters: + /// - value: The value to pass through. + /// - id: A value that uniquely identifies the represented expression in the + /// context of the expectation currently being evaluated. + /// + /// - Returns: `value`, verbatim. + /// + /// - Warning: This function is used to implement the `#expect()` and + /// `#require()` macros. Do not call it directly. + @inlinable public mutating func callAsFunction(_ value: T, _ id: __ExpressionID) -> T { + captureValue(value, id) + } + +#if SWT_SUPPORTS_MOVE_ONLY_EXPRESSION_EXPANSION + /// Capture information about a value for use if the expectation currently + /// being evaluated fails. + /// + /// - Parameters: + /// - value: The value to pass through. + /// - id: A value that uniquely identifies the represented expression in the + /// context of the expectation currently being evaluated. + /// + /// - Returns: `value`, verbatim. + /// + /// - Warning: This function is used to implement the `#expect()` and + /// `#require()` macros. Do not call it directly. + @_disfavoredOverload + public mutating func callAsFunction(_ value: consuming T, _ id: __ExpressionID) -> T where T: ~Copyable { + // TODO: add support for borrowing non-copyable expressions (need @lifetime) + return value + } +#endif + + /// Capture information about a value passed `inout` to a function call after + /// the function has returned. + /// + /// - Parameters: + /// - value: The value that was passed `inout` (i.e. with the `&` operator.) + /// - id: A value that uniquely identifies the represented expression in the + /// context of the expectation currently being evaluated. + /// + /// - Warning: This function is used to implement the `#expect()` and + /// `#require()` macros. Do not call it directly. + public mutating func __inoutAfter(_ value: T, _ id: __ExpressionID) { + runtimeValues[id] = { Expression.Value(reflecting: value, timing: .after) } + } +} + +// MARK: - Collection comparison and diffing + +extension __ExpectationContext { + /// Generate a description of a previously-computed collection difference. + /// + /// - Parameters: + /// - difference: The difference to describe. + /// + /// - Returns: A human-readable string describing `difference`. + private static func _description(of difference: CollectionDifference) -> String { + let insertions: [String] = difference.insertions.lazy + .map(\.element) + .map(String.init(describingForTest:)) + let removals: [String] = difference.removals.lazy + .map(\.element) + .map(String.init(describingForTest:)) + + var resultComponents = [String]() + if !insertions.isEmpty { + resultComponents.append("inserted [\(insertions.joined(separator: ", "))]") + } + if !removals.isEmpty { + resultComponents.append("removed [\(removals.joined(separator: ", "))]") + } + + return resultComponents.joined(separator: ", ") + } + + /// Compare two values using `==` or `!=`. + /// + /// - Parameters: + /// - lhs: The left-hand operand. + /// - lhsID: A value that uniquely identifies the expression represented by + /// `lhs` in the context of the expectation currently being evaluated. + /// - rhs: The left-hand operand. + /// - rhsID: A value that uniquely identifies the expression represented by + /// `rhs` in the context of the expectation currently being evaluated. + /// - op: A function that performs an operation on `lhs` and `rhs`. + /// - opID: A value that uniquely identifies the expression represented by + /// `op` in the context of the expectation currently being evaluated. + /// + /// - Returns: The result of calling `op(lhs, rhs)`. + /// + /// This overload of `__cmp()` serves as a catch-all for operands that are not + /// collections or otherwise are not interesting to the testing library. + /// + /// - Warning: This function is used to implement the `#expect()` and + /// `#require()` macros. Do not call it directly. + @inlinable public mutating func __cmp( + _ op: (T, U) throws -> R, + _ opID: __ExpressionID, + _ lhs: T, + _ lhsID: __ExpressionID, + _ rhs: U, + _ rhsID: __ExpressionID + ) rethrows -> R { + try captureValue(op(captureValue(lhs, lhsID), captureValue(rhs, rhsID)), opID) + } + + /// Compare two bidirectional collections using `==` or `!=`. + /// + /// This overload of `__cmp()` performs a diffing operation on `lhs` and `rhs` + /// if the result of `op(lhs, rhs)` is `false`. + /// + /// - Warning: This function is used to implement the `#expect()` and + /// `#require()` macros. Do not call it directly. + public mutating func __cmp( + _ op: (C, C) -> Bool, + _ opID: __ExpressionID, + _ lhs: C, + _ lhsID: __ExpressionID, + _ rhs: C, + _ rhsID: __ExpressionID + ) -> Bool where C: BidirectionalCollection, C.Element: Equatable { + let result = captureValue(op(captureValue(lhs, lhsID), captureValue(rhs, rhsID)), opID) + + if !result { + differences[opID] = { CollectionDifference(lhs.difference(from: rhs)) } + } + + return result + } + + /// Compare two range expressions using `==` or `!=`. + /// + /// This overload of `__cmp()` does _not_ perform a diffing operation on `lhs` + /// and `rhs`. Range expressions are not usefully diffable the way other kinds + /// of collections are. ([#639](https://github.com/swiftlang/swift-testing/issues/639)) + /// + /// - Warning: This function is used to implement the `#expect()` and + /// `#require()` macros. Do not call it directly. + @inlinable public mutating func __cmp( + _ op: (R, R) -> Bool, + _ opID: __ExpressionID, + _ lhs: R, + _ lhsID: __ExpressionID, + _ rhs: R, + _ rhsID: __ExpressionID + ) -> Bool where R: RangeExpression & BidirectionalCollection, R.Element: Equatable { + captureValue(op(captureValue(lhs, lhsID), captureValue(rhs, rhsID)), opID) + } + + /// Compare two strings using `==` or `!=`. + /// + /// This overload of `__cmp()` performs a diffing operation on `lhs` and `rhs` + /// if the result of `op(lhs, rhs)` is `false`, but does so by _line_, not by + /// _character_. + /// + /// - Warning: This function is used to implement the `#expect()` and + /// `#require()` macros. Do not call it directly. + public mutating func __cmp( + _ op: (S, S) -> Bool, + _ opID: __ExpressionID, + _ lhs: S, + _ lhsID: __ExpressionID, + _ rhs: S, + _ rhsID: __ExpressionID + ) -> Bool where S: StringProtocol { + let result = captureValue(op(captureValue(lhs, lhsID), captureValue(rhs, rhsID)), opID) + + if !result { + differences[opID] = { + // Compare strings by line, not by character. + let lhsLines = String(lhs).split(whereSeparator: \.isNewline) + let rhsLines = String(rhs).split(whereSeparator: \.isNewline) + + if lhsLines.count == 1 && rhsLines.count == 1 { + // There are no newlines in either string, so there's no meaningful + // per-line difference. Bail. + return nil + } + + let diff = lhsLines.difference(from: rhsLines) + if diff.isEmpty { + // The strings must have compared on a per-character basis, or this + // operator doesn't behave the way we expected. Bail. + return nil + } + + return CollectionDifference(diff) + } + } + + return result + } +} + +// MARK: - Casting + +extension __ExpectationContext { + /// Perform a conditional cast (`as?`) on a value. + /// + /// - Parameters: + /// - value: The value to cast. + /// - valueID: A value that uniquely identifies the expression represented + /// by `value` in the context of the expectation being evaluated. + /// - type: The type to cast `value` to. + /// - valueID: A value that uniquely identifies the expression represented + /// by `type` in the context of the expectation being evaluated. + /// + /// - Returns: The result of the expression `value as? type`. + /// + /// If `value` cannot be cast to `type`, the previously-recorded context for + /// the expression `type` is assigned the runtime value `type(of: value)` so + /// that the _actual_ type of `value` is recorded in any resulting issue. + /// + /// - Warning: This function is used to implement the `#expect()` and + /// `#require()` macros. Do not call it directly. + @inlinable public mutating func __as(_ value: T, _ valueID: __ExpressionID, _ type: U.Type, _ typeID: __ExpressionID) -> U? { + let result = captureValue(value, valueID) as? U + + if result == nil { + let correctType = Swift.type(of: value as Any) + _ = captureValue(correctType, typeID) + } + + return result + } + + /// Check the type of a value using the `is` operator. + /// + /// - Parameters: + /// - value: The value to cast. + /// - valueID: A value that uniquely identifies the expression represented + /// by `value` in the context of the expectation being evaluated. + /// - type: The type `value` is expected to be. + /// - valueID: A value that uniquely identifies the expression represented + /// by `type` in the context of the expectation being evaluated. + /// + /// - Returns: The result of the expression `value as? type`. + /// + /// If `value` is not an instance of `type`, the previously-recorded context + /// for the expression `type` is assigned the runtime value `type(of: value)` + /// so that the _actual_ type of `value` is recorded in any resulting issue. + /// + /// - Warning: This function is used to implement the `#expect()` and + /// `#require()` macros. Do not call it directly. + @inlinable public mutating func __is(_ value: T, _ valueID: __ExpressionID, _ type: U.Type, _ typeID: __ExpressionID) -> Bool { + let result = captureValue(value, valueID) is U + + if !result { + let correctType = Swift.type(of: value as Any) + _ = captureValue(correctType, typeID) + } + + return result + } +} diff --git a/Sources/Testing/SourceAttribution/Expression+Macro.swift b/Sources/Testing/SourceAttribution/Expression+Macro.swift deleted file mode 100644 index 8bbd46b13..000000000 --- a/Sources/Testing/SourceAttribution/Expression+Macro.swift +++ /dev/null @@ -1,105 +0,0 @@ -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2023 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for Swift project authors -// - -extension __Expression { - /// Create an instance of this type representing a complete syntax node. - /// - /// - Parameters: - /// - syntaxNode: The complete syntax node (expression, declaration, - /// statement, etc.) - /// - /// - Returns: A new instance of this type. - /// - /// - Warning: This function is used to implement the `@Test`, `@Suite`, - /// `#expect()` and `#require()` macros. Do not call it directly. - public static func __fromSyntaxNode(_ syntaxNode: String) -> Self { - Self(kind: .generic(syntaxNode)) - } - - /// Create an instance of this type representing a string literal. - /// - /// - Parameters: - /// - sourceCode: The source code representation of the string literal, - /// including leading and trailing punctuation. - /// - stringValue: The actual string value of the string literal - /// - /// - Returns: A new instance of this type. - /// - /// - Warning: This function is used to implement the `@Test`, `@Suite`, - /// `#expect()` and `#require()` macros. Do not call it directly. - public static func __fromStringLiteral(_ sourceCode: String, _ stringValue: String) -> Self { - Self(kind: .stringLiteral(sourceCode: sourceCode, stringValue: stringValue)) - } - - /// Create an instance of this type representing a binary operation. - /// - /// - Parameters: - /// - lhs: The left-hand operand. - /// - op: The operator. - /// - rhs: The right-hand operand. - /// - /// - Returns: A new instance of this type. - /// - /// - Warning: This function is used to implement the `@Test`, `@Suite`, - /// `#expect()` and `#require()` macros. Do not call it directly. - public static func __fromBinaryOperation(_ lhs: Self, _ op: String, _ rhs: Self) -> Self { - return Self(kind: .binaryOperation(lhs: lhs, operator: op, rhs: rhs)) - } - - /// Create an instance of this type representing a function call. - /// - /// - Parameters: - /// - value: The value on which the member function is being invoked, if - /// any. - /// - functionName: The name of the member function. - /// - argumentLabel: Optionally, the argument label. - /// - argumentValue: The value of the argument to the function. - /// - /// - Returns: A new instance of this type. - /// - /// - Warning: This function is used to implement the `@Test`, `@Suite`, - /// `#expect()` and `#require()` macros. Do not call it directly. - public static func __fromFunctionCall(_ value: Self?, _ functionName: String, _ arguments: (label: String?, value: Self)...) -> Self { - let arguments = arguments.map(Kind.FunctionCallArgument.init) - return Self(kind: .functionCall(value: value, functionName: functionName, arguments: arguments)) - } - - /// Create an instance of this type representing a property access. - /// - /// - Parameters: - /// - value: The value whose property was accessed. - /// - keyPath: The key path, relative to `value`, that was accessed, not - /// including a leading backslash or period. - /// - /// - Returns: A new instance of this type. - /// - /// - Warning: This function is used to implement the `@Test`, `@Suite`, - /// `#expect()` and `#require()` macros. Do not call it directly. - public static func __fromPropertyAccess(_ value: Self, _ keyPath: Self) -> Self { - return Self(kind: .propertyAccess(value: value, keyPath: keyPath)) - } - - /// Create an instance of this type representing a negated expression - /// using the `!` operator.. - /// - /// - Parameters: - /// - expression: The expression that was negated. - /// - isParenthetical: Whether or not `expression` was enclosed in - /// parentheses (and the `!` operator was outside it.) This argument - /// affects how this expression is represented as a string. - /// - /// - Returns: A new instance of this type. - /// - /// - Warning: This function is used to implement the `@Test`, `@Suite`, - /// `#expect()` and `#require()` macros. Do not call it directly. - public static func __fromNegation(_ expression: Self, _ isParenthetical: Bool) -> Self { - return Self(kind: .negation(expression, isParenthetical: isParenthetical)) - } -} diff --git a/Sources/Testing/SourceAttribution/Expression.swift b/Sources/Testing/SourceAttribution/Expression.swift index dce4ed2a2..d73a47d00 100644 --- a/Sources/Testing/SourceAttribution/Expression.swift +++ b/Sources/Testing/SourceAttribution/Expression.swift @@ -36,64 +36,6 @@ public struct __Expression: Sendable { /// - Parameters: /// - sourceCode: The source code of the represented expression. case generic(_ sourceCode: String) - - /// The expression represents a string literal expression. - /// - /// - Parameters: - /// - sourceCode: The source code of the represented expression. Note that - /// this string is not the _value_ of the string literal, but the string - /// literal itself (including leading and trailing quote marks and - /// extended punctuation.) - /// - stringValue: The value of the string literal. - case stringLiteral(sourceCode: String, stringValue: String) - - /// The expression represents a binary operation. - /// - /// - Parameters: - /// - lhs: The left-hand operand. - /// - operator: The operator. - /// - rhs: The right-hand operand. - indirect case binaryOperation(lhs: __Expression, `operator`: String, rhs: __Expression) - - /// A type representing an argument to a function call, used by the - /// `__Expression.Kind.functionCall` case. - /// - /// This type is not part of the public interface of the testing library. - struct FunctionCallArgument: Sendable { - /// The label, if present, of the argument. - var label: String? - - /// The value, as an expression, of the argument. - var value: __Expression - } - - /// The expression represents a function call. - /// - /// - Parameters: - /// - value: The value on which the function was called, if any. - /// - functionName: The name of the function that was called. - /// - arguments: The arguments passed to the function. - indirect case functionCall(value: __Expression?, functionName: String, arguments: [FunctionCallArgument]) - - /// The expression represents a property access. - /// - /// - Parameters: - /// - value: The value whose property was accessed. - /// - keyPath: The key path, relative to `value`, that was accessed, not - /// including a leading backslash or period. - indirect case propertyAccess(value: __Expression, keyPath: __Expression) - - /// The expression negates another expression. - /// - /// - Parameters: - /// - expression: The expression that was negated. - /// - isParenthetical: Whether or not `expression` was enclosed in - /// parentheses (and the `!` operator was outside it.) This argument - /// affects how this expression is represented as a string. - /// - /// Unlike other cases in this enumeration, this case affects the runtime - /// behavior of the `__check()` family of functions. - indirect case negation(_ expression: __Expression, isParenthetical: Bool) } /// The kind of syntax node represented by this instance. @@ -107,30 +49,8 @@ public struct __Expression: Sendable { @_spi(ForToolsIntegrationOnly) public var sourceCode: String { switch kind { - case let .generic(sourceCode), let .stringLiteral(sourceCode, _): + case let .generic(sourceCode): return sourceCode - case let .binaryOperation(lhs, op, rhs): - return "\(lhs) \(op) \(rhs)" - case let .functionCall(value, functionName, arguments): - let argumentList = arguments.lazy - .map { argument in - if let argumentLabel = argument.label { - return "\(argumentLabel): \(argument.value.sourceCode)" - } - return argument.value.sourceCode - }.joined(separator: ", ") - if let value { - return "\(value.sourceCode).\(functionName)(\(argumentList))" - } - return "\(functionName)(\(argumentList))" - case let .propertyAccess(value, keyPath): - return "\(value.sourceCode).\(keyPath.sourceCode)" - case let .negation(expression, isParenthetical): - var sourceCode = expression.sourceCode - if isParenthetical { - sourceCode = "(\(sourceCode))" - } - return "!\(sourceCode)" } } @@ -149,6 +69,21 @@ public struct __Expression: Sendable { /// Information about the type of this value. public var typeInfo: TypeInfo + /// The timing when a runtime value was captured. + @_spi(Experimental) + public enum Timing: String, Sendable { + /// The value was captured after the containing expression was evaluated. + case after + } + + /// When the value represented by this instance was captured. + /// + /// The value of this property is typically `nil`. It may be set to a + /// non-`nil` value if this instance represents some `inout` argument passed + /// to a function with the `&` operator. + @_spi(Experimental) + public var timing: Timing? + /// The label associated with this value, if any. /// /// For non-child instances, or for child instances of members who do not @@ -210,14 +145,15 @@ public struct __Expression: Sendable { /// /// - Parameters: /// - subject: The subject this instance should reflect. - init?(reflecting subject: Any) { + /// - timing: When the value represented by this instance was captured. + init?(reflecting subject: Any, timing: Timing? = nil) { let configuration = Configuration.current ?? .init() guard let options = configuration.valueReflectionOptions else { return nil } var seenObjects: [ObjectIdentifier: AnyObject] = [:] - self.init(_reflecting: subject, label: nil, seenObjects: &seenObjects, depth: 0, options: options) + self.init(_reflecting: subject, label: nil, timing: timing, seenObjects: &seenObjects, depth: 0, options: options) } /// Initialize an instance of this type describing the specified subject and @@ -228,6 +164,7 @@ public struct __Expression: Sendable { /// - label: An optional label for this value. This should be a non-`nil` /// value when creating instances of this type which describe /// substructural values. + /// - timing: When the value represented by this instance was captured. /// - seenObjects: The objects which have been seen so far while calling /// this initializer recursively, keyed by their object identifiers. /// This is used to halt further recursion if a previously-seen object @@ -238,6 +175,7 @@ public struct __Expression: Sendable { private init( _reflecting subject: Any, label: String?, + timing: Timing?, seenObjects: inout [ObjectIdentifier: AnyObject], depth: Int, options: Configuration.ValueReflectionOptions @@ -306,7 +244,7 @@ public struct __Expression: Sendable { break } - children.append(Self(_reflecting: child.value, label: child.label, seenObjects: &seenObjects, depth: depth + 1, options: options)) + children.append(Self(_reflecting: child.value, label: child.label, timing: timing, seenObjects: &seenObjects, depth: depth + 1, options: options)) } self.children = children } @@ -320,235 +258,72 @@ public struct __Expression: Sendable { @_spi(ForToolsIntegrationOnly) public var runtimeValue: Value? - /// Copy this instance and capture the runtime value corresponding to it. - /// - /// - Parameters: - /// - value: The captured runtime value. - /// - /// - Returns: A copy of `self` with information about the specified runtime - /// value captured for future use. - func capturingRuntimeValue(_ value: (some Any)?) -> Self { - var result = self - result.runtimeValue = value.flatMap(Value.init(reflecting:)) - if case let .negation(subexpression, isParenthetical) = kind, let value = value as? Bool { - result.kind = .negation(subexpression.capturingRuntimeValue(!value), isParenthetical: isParenthetical) - } - return result - } - - /// Copy this instance and capture the runtime values corresponding to its - /// subexpressions. - /// - /// - Parameters: - /// - firstValue: The first captured runtime value. - /// - additionalValues: Any additional captured runtime values after the - /// first. - /// - /// - Returns: A copy of `self` with information about the specified runtime - /// values captured for future use. - /// - /// If the ``kind`` of `self` is ``Kind/generic`` or ``Kind/stringLiteral``, - /// this function is equivalent to ``capturingRuntimeValue(_:)``. - func capturingRuntimeValues(_ firstValue: (some Any)?, _ additionalValues: repeat (each T)?) -> Self { - var result = self - - // Convert the variadic generic argument list to an array. - var additionalValuesArray = [Any?]() - repeat additionalValuesArray.append(each additionalValues) - - switch kind { - case .generic, .stringLiteral: - result = capturingRuntimeValue(firstValue) - case let .binaryOperation(lhsExpr, op, rhsExpr): - result.kind = .binaryOperation( - lhs: lhsExpr.capturingRuntimeValues(firstValue), - operator: op, - rhs: rhsExpr.capturingRuntimeValues(additionalValuesArray.first ?? nil) - ) - case let .functionCall(value, functionName, arguments): - result.kind = .functionCall( - value: value?.capturingRuntimeValues(firstValue), - functionName: functionName, - arguments: zip(arguments, additionalValuesArray).map { argument, value in - .init(label: argument.label, value: argument.value.capturingRuntimeValues(value)) - } - ) - case let .propertyAccess(value, keyPath): - result.kind = .propertyAccess( - value: value.capturingRuntimeValues(firstValue), - keyPath: keyPath.capturingRuntimeValues(additionalValuesArray.first ?? nil) - ) - case let .negation(expression, isParenthetical): - result.kind = .negation( - expression.capturingRuntimeValues(firstValue, repeat each additionalValues), - isParenthetical: isParenthetical - ) - } - - return result - } - /// Get an expanded description of this instance that contains the source /// code and runtime value (or values) it represents. /// /// - Returns: A string describing this instance. @_spi(ForToolsIntegrationOnly) public func expandedDescription() -> String { - _expandedDescription(in: _ExpandedDescriptionContext()) - } - - /// Get an expanded description of this instance that contains the source - /// code and runtime value (or values) it represents. - /// - /// - Returns: A string describing this instance. - /// - /// This function produces a more detailed description than - /// ``expandedDescription()``, similar to how `String(reflecting:)` produces - /// a more detailed description than `String(describing:)`. - func expandedDebugDescription() -> String { - var context = _ExpandedDescriptionContext() - context.includeTypeNames = true - context.includeParenthesesIfNeeded = false - return _expandedDescription(in: context) - } - - /// A structure describing the state tracked while calling - /// `_expandedDescription(in:)`. - private struct _ExpandedDescriptionContext { - /// The depth of recursion at which the function is being called. - var depth = 0 - - /// Whether or not to include type names in output. - var includeTypeNames = false - - /// Whether or not to enclose the resulting string in parentheses (as needed - /// depending on what information the resulting string contains.) - var includeParenthesesIfNeeded = true + expandedDescription(verbose: false) } /// Get an expanded description of this instance that contains the source /// code and runtime value (or values) it represents. /// /// - Parameters: - /// - context: The context for this call. + /// - verbose: Whether or not to include more verbose output. /// /// - Returns: A string describing this instance. /// - /// This function provides the implementation of ``expandedDescription()`` and - /// ``expandedDebugDescription()``. - private func _expandedDescription(in context: _ExpandedDescriptionContext) -> String { - // Create a (default) context value to pass to recursive calls for - // subexpressions. - var childContext = context - do { - // Bump the depth so that recursive calls track the next depth level. - childContext.depth += 1 - - // Subexpressions do not automatically disable parentheses if the parent - // does; they must opt in. - childContext.includeParenthesesIfNeeded = true - } - - var result = "" - switch kind { - case let .generic(sourceCode), let .stringLiteral(sourceCode, _): - result = if context.includeTypeNames, let qualifiedName = runtimeValue?.typeInfo.fullyQualifiedName { - "\(sourceCode): \(qualifiedName)" - } else { - sourceCode - } - case let .binaryOperation(lhsExpr, op, rhsExpr): - result = "\(lhsExpr._expandedDescription(in: childContext)) \(op) \(rhsExpr._expandedDescription(in: childContext))" - case let .functionCall(value, functionName, arguments): - var argumentContext = childContext - argumentContext.includeParenthesesIfNeeded = (arguments.count > 1) - let argumentList = arguments.lazy - .map { argument in - (argument.label, argument.value._expandedDescription(in: argumentContext)) - }.map { label, value in - if let label { - return "\(label): \(value)" - } - return value - }.joined(separator: ", ") - result = if let value { - "\(value._expandedDescription(in: childContext)).\(functionName)(\(argumentList))" - } else { - "\(functionName)(\(argumentList))" - } - case let .propertyAccess(value, keyPath): - var keyPathContext = childContext - keyPathContext.includeParenthesesIfNeeded = false - result = "\(value._expandedDescription(in: childContext)).\(keyPath._expandedDescription(in: keyPathContext))" - case let .negation(expression, isParenthetical): - childContext.includeParenthesesIfNeeded = !isParenthetical - var expandedDescription = expression._expandedDescription(in: childContext) - if isParenthetical { - expandedDescription = "(\(expandedDescription))" - } - result = "!\(expandedDescription)" - } + /// This function provides the implementation of ``expandedDescription()`` + /// with additional options used by ``Event/HumanReadableOutputRecorder``. + func expandedDescription(verbose: Bool) -> String { + var result = sourceCode - // If this expression is at the root of the expression graph... - if context.depth == 0 { - if runtimeValue == nil { - // ... and has no value, don't bother reporting the placeholder string - // for it... - return result - } else if let runtimeValue, runtimeValue.typeInfo.describes(Bool.self) { - // ... or if it is a boolean value, also don't bother (because it can be - // inferred from context.) - return result - } + if verbose, let qualifiedName = runtimeValue?.typeInfo.fullyQualifiedName { + result = "\(result): \(qualifiedName)" } - let runtimeValueDescription = runtimeValue.map(String.init(describing:)) ?? "" - result = if runtimeValueDescription == "(Function)" { + if let runtimeValue { + let runtimeValueDescription = String(describingForTest: runtimeValue) // Hack: don't print string representations of function calls. - result - } else if runtimeValueDescription == result { - result - } else if context.includeParenthesesIfNeeded && context.depth > 0 { - "(\(result) → \(runtimeValueDescription))" + if runtimeValueDescription != "(Function)" && runtimeValueDescription != result { + switch runtimeValue.timing { + case .after: + result = "\(result) (after)" + default: + break + } + result = "\(result) → \(runtimeValueDescription)" + } } else { - "\(result) → \(runtimeValueDescription)" + result = "\(result) → " } + return result } /// The set of parsed and captured subexpressions contained in this instance. @_spi(ForToolsIntegrationOnly) - public var subexpressions: [Self] { - switch kind { - case .generic, .stringLiteral: - [] - case let .binaryOperation(lhs, _, rhs): - [lhs, rhs] - case let .functionCall(value, _, arguments): - if let value { - CollectionOfOne(value) + arguments.lazy.map(\.value) - } else { - arguments.lazy.map(\.value) - } - case let .propertyAccess(value: value, keyPath: keyPath): - [value, keyPath] - case let .negation(expression, _): - [expression] - } - } + public internal(set) var subexpressions = [Self]() - /// The string value associated with this instance if it represents a string - /// literal. + /// A description of the difference between the operands in this expression, + /// if that difference could be determined. + /// + /// The value of this property is set for the binary operators `==` and `!=` + /// when used to compare collections. /// - /// If this instance represents an expression other than a string literal, the - /// value of this property is `nil`. + /// If the containing expectation passed, the value of this property is `nil` + /// because the difference is only computed when necessary to assist with + /// diagnosing test failures. + @_spi(Experimental) @_spi(ForToolsIntegrationOnly) + public internal(set) var differenceDescription: String? + @_spi(ForToolsIntegrationOnly) + @available(*, deprecated, message: "The value of this property is always nil.") public var stringLiteralValue: String? { - if case let .stringLiteral(_, stringValue) = kind { - return stringValue - } - return nil + nil } } @@ -556,8 +331,8 @@ public struct __Expression: Sendable { extension __Expression: Codable {} extension __Expression.Kind: Codable {} -extension __Expression.Kind.FunctionCallArgument: Codable {} extension __Expression.Value: Codable {} +extension __Expression.Value.Timing: Codable {} // MARK: - CustomStringConvertible, CustomDebugStringConvertible diff --git a/Sources/Testing/SourceAttribution/ExpressionID.swift b/Sources/Testing/SourceAttribution/ExpressionID.swift new file mode 100644 index 000000000..189594c6f --- /dev/null +++ b/Sources/Testing/SourceAttribution/ExpressionID.swift @@ -0,0 +1,171 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +/// A type providing unique identifiers for expressions captured during +/// expansion of the `#expect()` and `#require()` macros. +/// +/// This type tries to optimize for expressions in shallow syntax trees whose +/// unique identifiers require 64 bits or fewer. Wider unique identifiers are +/// stored as arrays of 64-bit words. In the future, this type may use +/// [`StaticBigInt`](https://developer.apple.com/documentation/swift/staticbigint) +/// to represent expression identifiers instead. +/// +/// - Warning: This type is used to implement the `#expect()` and `#require()` +/// macros. Do not use it directly. +public struct __ExpressionID: Sendable { + /// The ID of the root node in an expression graph. + static var root: Self { + Self(elements: .none) + } + + /// An enumeration that attempts to efficiently store the key path elements + /// corresponding to an expression ID. + fileprivate enum Elements: Sendable { + /// This ID does not use any words. + /// + /// This case represents the root node in a syntax tree. An instance of + /// `__ExpressionID` storing this case is implicitly equal to `.root`. + case none + + /// This ID packs its corresponding key path value into a single word whose + /// value is not `0`. + case packed(_ word: UInt64) + + /// This ID contains key path elements that do not fit in a 64-bit integer, + /// so they are not packed and map directly to the represented key path. + indirect case keyPath(_ keyPath: [UInt32]) + } + + /// The elements of this identifier. + fileprivate var elements: Elements +} + +// MARK: - Equatable, Hashable + +extension __ExpressionID: Equatable, Hashable {} +extension __ExpressionID.Elements: Equatable, Hashable {} + +// MARK: - Collection + +extension __ExpressionID { + /// A type representing the elements in a key path produced from the unique + /// identifier of an expression. + /// + /// Instances of this type can be used to produce keys and key paths for an + /// instance of `Graph` whose key type is `UInt32`. + private struct _KeyPathForGraph: Collection { + /// Underlying storage for the collection. + var elements: __ExpressionID.Elements + + var count: Int { + switch elements { + case .none: + 0 + case let .packed(word): + word.nonzeroBitCount + case let .keyPath(keyPath): + keyPath.count + } + } + + var startIndex: Int { + switch elements { + case .none, .keyPath: + 0 + case let .packed(word): + word.trailingZeroBitCount + } + } + + var endIndex: Int { + switch elements { + case .none: + 0 + case .packed: + UInt64.bitWidth + case let .keyPath(keyPath): + keyPath.count + } + } + + func index(after i: Int) -> Int { + let uncheckedNextIndex = i + 1 + switch elements { + case .none, .keyPath: + return uncheckedNextIndex + case let .packed(word): + // Mask off the low bits including the one at `i`. The trailing zero + // count of the resulting value equals the next actual bit index. + let maskedWord = word & (~0 << uncheckedNextIndex) + return maskedWord.trailingZeroBitCount + } + } + + subscript(position: Int) -> UInt32 { + switch elements { + case .none: + fatalError("Unreachable") + case .packed: + UInt32(position) + case let .keyPath(keyPath): + keyPath[position] + } + } + } + + /// A representation of this instance suitable for use as a key path in an + /// instance of `Graph` where the key type is `UInt32`. + /// + /// The values in this collection, being swift-syntax node IDs, are never more + /// than 32 bits wide. + var keyPathRepresentation: some Collection { + _KeyPathForGraph(elements: elements) + } +} + +#if DEBUG +// MARK: - CustomStringConvertible, CustomDebugStringConvertible + +extension __ExpressionID: CustomStringConvertible, CustomDebugStringConvertible { + public var description: String { + switch elements { + case .none: + return "0" + case let .packed(word): + return "0x\(String(word, radix: 16))" + case let .keyPath(keyPath): + let components: String = keyPath.lazy + .map { String($0, radix: 16) } + .joined(separator: ",") + return "[\(components)]" + } + } + + public var debugDescription: String { + #""\#(description)" → \#(Array(keyPathRepresentation))"# + } +} +#endif + +// MARK: - ExpressibleByIntegerLiteral + +extension __ExpressionID: ExpressibleByIntegerLiteral { + public init(integerLiteral: UInt64) { + if integerLiteral == 0 { + self.init(elements: .none) + } else { + self.init(elements: .packed(integerLiteral)) + } + } + + public init(_ keyPath: UInt32...) { + self.init(elements: .keyPath(keyPath)) + } +} diff --git a/Sources/Testing/Support/Additions/CollectionDifferenceAdditions.swift b/Sources/Testing/Support/Additions/CollectionDifferenceAdditions.swift index 94a09b14c..df942fbca 100644 --- a/Sources/Testing/Support/Additions/CollectionDifferenceAdditions.swift +++ b/Sources/Testing/Support/Additions/CollectionDifferenceAdditions.swift @@ -8,6 +8,28 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // +extension CollectionDifference { + /// Convert an instance of `CollectionDifference` to one that is type-erased + /// over elements of type `Any`. + /// + /// - Parameters: + /// - difference: The difference to convert. + init(_ difference: CollectionDifference) { + self.init( + difference.lazy.map { change in + switch change { + case let .insert(offset, element, associatedWith): + return .insert(offset: offset, element: element as Any, associatedWith: associatedWith) + case let .remove(offset, element, associatedWith): + return .remove(offset: offset, element: element as Any, associatedWith: associatedWith) + } + } + )! + } +} + +// MARK: - + extension CollectionDifference.Change { /// The element that was changed. var element: ChangeElement { diff --git a/Sources/Testing/Support/Additions/ResultAdditions.swift b/Sources/Testing/Support/Additions/ResultAdditions.swift index 9a2e6ea5a..e83e0f7ab 100644 --- a/Sources/Testing/Support/Additions/ResultAdditions.swift +++ b/Sources/Testing/Support/Additions/ResultAdditions.swift @@ -8,18 +8,18 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -extension Result { +extension Result where Success: ~Copyable { /// Handle this instance as if it were returned from a call to `#expect()`. /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. - @inlinable public func __expected() where Success == Void {} + @inlinable public borrowing func __expected() where Success == Void {} /// Handle this instance as if it were returned from a call to `#require()`. /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. - @inlinable public func __required() throws -> Success { + @inlinable public consuming func __required() throws -> Success { try get() } } diff --git a/Sources/TestingMacros/CMakeLists.txt b/Sources/TestingMacros/CMakeLists.txt index 4fc8b3b58..2022e6cf9 100644 --- a/Sources/TestingMacros/CMakeLists.txt +++ b/Sources/TestingMacros/CMakeLists.txt @@ -87,7 +87,9 @@ target_sources(TestingMacros PRIVATE Support/Additions/DeclGroupSyntaxAdditions.swift Support/Additions/EditorPlaceholderExprSyntaxAdditions.swift Support/Additions/FunctionDeclSyntaxAdditions.swift + Support/Additions/IntegerLiteralExprSyntaxAdditions.swift Support/Additions/MacroExpansionContextAdditions.swift + Support/Additions/SyntaxProtocolAdditions.swift Support/Additions/TokenSyntaxAdditions.swift Support/Additions/TriviaPieceAdditions.swift Support/Additions/TypeSyntaxProtocolAdditions.swift @@ -101,7 +103,7 @@ target_sources(TestingMacros PRIVATE Support/CRC32.swift Support/DiagnosticMessage.swift Support/DiagnosticMessage+Diagnosing.swift - Support/SourceCodeCapturing.swift + Support/EffectfulExpressionHandling.swift Support/SourceLocationGeneration.swift TagMacro.swift TestDeclarationMacro.swift diff --git a/Sources/TestingMacros/ConditionMacro.swift b/Sources/TestingMacros/ConditionMacro.swift index 346cb68bf..b7558145a 100644 --- a/Sources/TestingMacros/ConditionMacro.swift +++ b/Sources/TestingMacros/ConditionMacro.swift @@ -39,6 +39,12 @@ public import SwiftSyntaxMacros public protocol ConditionMacro: ExpressionMacro, Sendable { /// Whether or not the macro's expansion may throw an error. static var isThrowing: Bool { get } + + /// The return type of the expansion's closure, if it can be statically + /// determined. + /// + /// This property is ignored when a condition macro is closure-based. + static var returnType: TypeSyntax? { get } } // MARK: - @@ -67,6 +73,15 @@ extension ConditionMacro { .disabled } + public static var returnType: TypeSyntax? { + TypeSyntax( + MemberTypeSyntax( + baseType: IdentifierTypeSyntax(name: .identifier("Swift")), + name: .identifier("Bool") + ) + ) + } + /// Perform the expansion of this condition macro. /// /// - Parameters: @@ -107,10 +122,12 @@ extension ConditionMacro { .firstIndex { $0.tokenKind == _sourceLocationLabel.tokenKind } // Construct the argument list to __check(). - let expandedFunctionName: TokenSyntax + var expandedFunctionName = TokenSyntax.identifier("__checkCondition") var checkArguments = [Argument]() + var effectKeywordsToApply: Set = [] do { if let trailingClosureIndex { + expandedFunctionName = .identifier("__checkClosureCall") // Include all arguments other than the "comment" and "sourceLocation" // arguments here. @@ -122,17 +139,87 @@ extension ConditionMacro { // The trailing closure should be the focus of the source code capture. let primaryExpression = primaryExpression ?? macroArguments[trailingClosureIndex].expression - let sourceCode = parseCondition(from: primaryExpression, for: macro, in: context).expression - checkArguments.append(Argument(label: "expression", expression: sourceCode)) + let nodeForSourceCodeArgument: Syntax + if let closureExpr = primaryExpression.as(ClosureExprSyntax.self), + closureExpr.signature == nil && closureExpr.statements.count == 1, + let item = closureExpr.statements.first?.item { + // TODO: capture closures as a different kind of Testing.Expression + // with a separate subexpression per code item. + + // If a closure contains a single statement or declaration, we can't + // meaningfully break it down as an expression, but we can still + // capture its source representation. + nodeForSourceCodeArgument = Syntax(item) + } else { + nodeForSourceCodeArgument = Syntax(primaryExpression) + } + checkArguments.append( + Argument( + label: "sourceCode", + expression: createDictionaryExpr(forSourceCodeOf: nodeForSourceCodeArgument) + ) + ) + + } else if let firstArgument = macroArguments.first { + let originalArgumentExpr = firstArgument.expression + effectKeywordsToApply = findEffectKeywords(in: originalArgumentExpr) + + var useEscapeHatch = false + if let asExpr = originalArgumentExpr.as(AsExprSyntax.self), asExpr.questionOrExclamationMark == nil { + // "Escape hatch" for x as Bool to avoid the full recursive expansion. + useEscapeHatch = true + } else if effectKeywordsToApply.contains(.consume) { + // `consume` expressions imply non-copyable values which cannot yet be + // safely used with the closure we generate below. + useEscapeHatch = true + } - expandedFunctionName = .identifier("__checkClosureCall") + if useEscapeHatch { + expandedFunctionName = .identifier("__checkEscapedCondition") - } else { - // Get the condition expression and extract its parsed form and source - // code. The first argument is always the condition argument if there is - // no trailing closure argument. - let conditionArgument = parseCondition(from: macroArguments.first!.expression, for: macro, in: context) - checkArguments += conditionArgument.arguments + checkArguments.append(firstArgument) + checkArguments.append( + Argument( + label: "sourceCode", + expression: createDictionaryExpr(forSourceCodeOf: originalArgumentExpr) + ) + ) + + } else { + if effectKeywordsToApply.contains(.await) { + expandedFunctionName = .identifier("__checkConditionAsync") + } + + var expressionContextName = TokenSyntax.identifier("__ec") + let isNameUsed = originalArgumentExpr.tokens(viewMode: .sourceAccurate).lazy + .map(\.tokenKind) + .contains(expressionContextName.tokenKind) + if isNameUsed { + // BUG: We should use the unique name directly. SEE: swift-syntax-#2256 + let uniqueName = context.makeUniqueName("") + expressionContextName = .identifier("\(expressionContextName)\(uniqueName)") + } + let (closureExpr, rewrittenNodes) = rewrite( + originalArgumentExpr, + usingExpressionContextNamed: expressionContextName, + for: macro, + rootedAt: originalArgumentExpr, + effectKeywordsToApply: effectKeywordsToApply, + returnType: returnType, + in: context + ) + checkArguments.append(Argument(expression: closureExpr)) + + checkArguments.append( + Argument( + label: "sourceCode", + expression: createDictionaryExpr( + forSourceCodeOf: rewrittenNodes, + rootedAt: originalArgumentExpr + ) + ) + ) + } // Include all arguments other than the "condition", "comment", and // "sourceLocation" arguments here. @@ -141,15 +228,6 @@ extension ConditionMacro { .filter { $0 != isolationArgumentIndex } .filter { $0 != sourceLocationArgumentIndex } .map { macroArguments[$0] } - - if let primaryExpression { - let sourceCode = parseCondition(from: primaryExpression, for: macro, in: context).expression - checkArguments.append(Argument(label: "expression", expression: sourceCode)) - } else { - checkArguments.append(Argument(label: "expression", expression: conditionArgument.expression)) - } - - expandedFunctionName = conditionArgument.expandedFunctionName } // Capture any comments as well (either in source or as a macro argument.) @@ -187,11 +265,19 @@ extension ConditionMacro { } // Construct and return the call to __check(). - let call: ExprSyntax = "Testing.\(expandedFunctionName)(\(LabeledExprListSyntax(checkArguments)))" - if isThrowing { - return "\(call).__required()" + var call: ExprSyntax = "Testing.\(expandedFunctionName)(\(LabeledExprListSyntax(checkArguments)))" + call = if isThrowing { + "\(call).__required()" + } else { + "\(call).__expected()" + } + if effectKeywordsToApply.contains(.await) { + call = "await \(call)" + } + if !isThrowing && effectKeywordsToApply.contains(.try) { + call = "try \(call)" } - return "\(call).__expected()" + return call } /// Get the complete argument list for a given macro, including any trailing @@ -255,6 +341,25 @@ public struct RequireMacro: ConditionMacro { } } +/// A type describing the expansion of the `#require()` macro when it produces +/// an optional value. +public struct UnwrapMacro: ConditionMacro { + public static var isThrowing: Bool { + true + } + + public static var returnType: TypeSyntax? { + TypeSyntax( + MemberTypeSyntax( + baseType: IdentifierTypeSyntax(name: .identifier("Swift")), + name: .identifier("Optional") + ) + ) + } +} + +// MARK: - Refined condition macros + /// A protocol that can be used to create a condition macro that refines the /// behavior of another previously-defined condition macro. public protocol RefinedConditionMacro: ConditionMacro { @@ -265,6 +370,10 @@ extension RefinedConditionMacro { public static var isThrowing: Bool { Base.isThrowing } + + public static var returnType: TypeSyntax? { + Base.returnType + } } // MARK: - Diagnostics-emitting condition macros @@ -274,7 +383,7 @@ extension RefinedConditionMacro { /// /// This type is otherwise exactly equivalent to ``RequireMacro``. public struct AmbiguousRequireMacro: RefinedConditionMacro { - public typealias Base = RequireMacro + public typealias Base = UnwrapMacro public static func expansion( of macro: some FreestandingMacroExpansionSyntax, @@ -285,7 +394,7 @@ public struct AmbiguousRequireMacro: RefinedConditionMacro { } // Perform the normal macro expansion for #require(). - return try RequireMacro.expansion(of: macro, in: context) + return try Base.expansion(of: macro, in: context) } /// Check for an ambiguous argument to the `#require()` macro and emit the @@ -317,7 +426,7 @@ public struct AmbiguousRequireMacro: RefinedConditionMacro { /// /// This type is otherwise exactly equivalent to ``RequireMacro``. public struct NonOptionalRequireMacro: RefinedConditionMacro { - public typealias Base = RequireMacro + public typealias Base = UnwrapMacro public static func expansion( of macro: some FreestandingMacroExpansionSyntax, @@ -328,7 +437,7 @@ public struct NonOptionalRequireMacro: RefinedConditionMacro { } // Perform the normal macro expansion for #require(). - return try RequireMacro.expansion(of: macro, in: context) + return try Base.expansion(of: macro, in: context) } } @@ -357,7 +466,7 @@ public struct RequireThrowsMacro: RefinedConditionMacro { } // Perform the normal macro expansion for #require(). - return try RequireMacro.expansion(of: macro, in: context) + return try Base.expansion(of: macro, in: context) } } @@ -377,7 +486,7 @@ public struct RequireThrowsNeverMacro: RefinedConditionMacro { } // Perform the normal macro expansion for #require(). - return try RequireMacro.expansion(of: macro, in: context) + return try Base.expansion(of: macro, in: context) } } @@ -428,7 +537,7 @@ extension ExitTestConditionMacro { decls.append( """ @Sendable func \(bodyThunkName)() async throws -> Void { - return try await Testing.__requiringTry(Testing.__requiringAwait(\(bodyArgumentExpr.trimmed)))() + return \(applyEffectfulKeywords([.try, .await], to: bodyArgumentExpr))() } """ ) @@ -453,8 +562,7 @@ extension ExitTestConditionMacro { arguments[trailingClosureIndex].expression = ExprSyntax( ClosureExprSyntax { for decl in decls { - CodeBlockItemSyntax(item: .decl(decl)) - .with(\.trailingTrivia, .newline) + decl.with(\.trailingTrivia, .newline) } } ) diff --git a/Sources/TestingMacros/Support/Additions/IntegerLiteralExprSyntaxAdditions.swift b/Sources/TestingMacros/Support/Additions/IntegerLiteralExprSyntaxAdditions.swift new file mode 100644 index 000000000..e2310b44f --- /dev/null +++ b/Sources/TestingMacros/Support/Additions/IntegerLiteralExprSyntaxAdditions.swift @@ -0,0 +1,18 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +import SwiftSyntax + +extension IntegerLiteralExprSyntax { + init(_ value: some BinaryInteger, radix: IntegerLiteralExprSyntax.Radix = .decimal) { + let stringValue = "\(radix.literalPrefix)\(String(value, radix: radix.size))" + self.init(literal: .integerLiteral(stringValue)) + } +} diff --git a/Sources/TestingMacros/Support/Additions/SyntaxProtocolAdditions.swift b/Sources/TestingMacros/Support/Additions/SyntaxProtocolAdditions.swift new file mode 100644 index 000000000..d1ce92565 --- /dev/null +++ b/Sources/TestingMacros/Support/Additions/SyntaxProtocolAdditions.swift @@ -0,0 +1,82 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +import SwiftSyntax +import SwiftSyntaxBuilder + +extension SyntaxProtocol { + /// Get an expression representing the unique ID of this syntax node as well + /// as those of its parent nodes. + /// + /// - Parameters: + /// - effectiveRootNode: The node to treat as the root of the syntax tree + /// for the purposes of generating a value. + /// + /// - Returns: An expression representing a bitmask of node IDs including this + /// node's and all ancestors (up to but excluding `effectiveRootNode`) + /// encoded as an instance of `String`. + func expressionID(rootedAt effectiveRootNode: some SyntaxProtocol) -> ExprSyntax { + // Construct the unique sequence of node IDs that leads to the node being + // rewritten. + var ancestralNodeIDs = sequence(first: Syntax(self), next: \.parent) + .map { $0.id.indexInTree.toOpaque() } + +#if DEBUG + assert(ancestralNodeIDs.sorted() == ancestralNodeIDs.reversed(), "Child node had lower ID than parent node in sequence \(ancestralNodeIDs). Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") + for id in ancestralNodeIDs { + assert(id <= UInt32.max, "Node ID \(id) was not a 32-bit integer. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") + } + + // The highest ID in the sequence determines the number of bits needed, and + // the ID of this node will always be the highest (per the assertion above.) + let expectedMaxID = id.indexInTree.toOpaque() + assert(ancestralNodeIDs.contains(expectedMaxID), "ID \(expectedMaxID) of syntax node '\(self.trimmed)' was not found in its node ID sequence \(ancestralNodeIDs). Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") +#endif + + // Adjust all node IDs downards by the effective root node's ID, then remove + // the effective root node and its ancestors. This allows us to use lower + // bit ranges than we would if we always included those nodes. + do { + let effRootNodeID = effectiveRootNode.id.indexInTree.toOpaque() + if let effRootNodeIndex = ancestralNodeIDs.lastIndex(of: effRootNodeID) { + ancestralNodeIDs = ancestralNodeIDs[.. (ExprSyntax, isParenthetical: Bool)? { +private func _negatedExpression(_ expr: ExprSyntax) -> ExprSyntax? { let expr = removeParentheses(from: expr) ?? expr if let op = expr.as(PrefixOperatorExprSyntax.self), op.operator.tokenKind == .prefixOperator("!") { if let negatedExpr = removeParentheses(from: op.expression) { - return (negatedExpr, true) + return negatedExpr } else { - return (op.expression, false) + return op.expression } } @@ -118,7 +80,7 @@ private func _negatedExpression(_ expr: ExprSyntax) -> (ExprSyntax, isParentheti /// not remove interior parentheses (e.g. `(foo, (bar))`.) func removeParentheses(from expr: ExprSyntax) -> ExprSyntax? { if let tuple = expr.as(TupleExprSyntax.self), - tuple.elements.count == 1, + tuple.elements.count == 1, let elementExpr = tuple.elements.first, elementExpr.label == nil { return removeParentheses(from: elementExpr.expression) ?? elementExpr.expression @@ -127,406 +89,745 @@ func removeParentheses(from expr: ExprSyntax) -> ExprSyntax? { return nil } -// MARK: - +// MARK: - Inserting expression context callouts -/// Parse a condition argument from a binary operation expression. -/// -/// - Parameters: -/// - expr: The expression to which `lhs` _et al._ belong. -/// - lhs: The left-hand operand expression. -/// - op: The operator expression. -/// - rhs: The right-hand operand expression. -/// - macro: The macro expression being expanded. -/// - context: The macro context in which the expression is being parsed. -/// -/// - Returns: An instance of ``Condition`` describing `expr`. -/// -/// This function currently only recognizes and converts simple binary operator -/// expressions. More complex expressions are treated as monolithic. -private func _parseCondition(from expr: ExprSyntax, leftOperand lhs: ExprSyntax, operator op: BinaryOperatorExprSyntax, rightOperand rhs: ExprSyntax, for macro: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> Condition { - return Condition( - "__checkBinaryOperation", - arguments: [ - Argument(expression: lhs), - Argument(expression: "{ $0 \(op.trimmed) $1() }"), - Argument(expression: rhs) - ], - expression: createExpressionExprForBinaryOperation(lhs, op, rhs) - ) -} +/// The maximum value of `_rewriteDepth` allowed by `_rewrite()` before it will +/// start bailing early. +private let _maximumRewriteDepth = { + Int.max // disable rewrite-limiting (need to evaluate possible heuristics) +}() -/// Parse a condition argument from an `is` expression. -/// -/// - Parameters: -/// - expr: The `is` expression. -/// - macro: The macro expression being expanded. -/// - context: The macro context in which the expression is being parsed. -/// -/// - Returns: An instance of ``Condition`` describing `expr`. -private func _parseCondition(from expr: IsExprSyntax, for macro: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> Condition { - let expression = expr.expression - let type = expr.type - - return Condition( - "__checkCast", - arguments: [ - Argument(expression: expression), - Argument(label: .identifier("is"), expression: "(\(type.trimmed)).self") - ], - expression: createExpressionExprForBinaryOperation(expression, expr.isKeyword, type) - ) -} +/// A type that inserts calls to an `__ExpectationContext` instance into an +/// expression's syntax tree. +private final class _ContextInserter: SyntaxRewriter where C: MacroExpansionContext, M: FreestandingMacroExpansionSyntax { + /// The macro context in which the expression is being parsed. + var context: C -/// Parse a condition argument from an `as?` expression. -/// -/// - Parameters: -/// - expr: The `as?` expression. -/// - macro: The macro expression being expanded. -/// - context: The macro context in which the expression is being parsed. -/// -/// - Returns: An instance of ``Condition`` describing `expr`. -private func _parseCondition(from expr: AsExprSyntax, for macro: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> Condition { - let expression = expr.expression - let type = expr.type - - switch expr.questionOrExclamationMark?.tokenKind { - case .postfixQuestionMark: - return Condition( - "__checkCast", - arguments: [ - Argument(expression: expression), - Argument(label: .identifier("as"), expression: "(\(type.trimmed)).self") - ], - expression: createExpressionExprForBinaryOperation(expression, TokenSyntax.unknown("as?"), type) - ) + /// The macro expression. + var macro: M - case .exclamationMark where !type.isNamed("Bool", inModuleNamed: "Swift") && !type.isOptional: - // Warn that as! will be evaluated before #expect() or #require(), which is - // probably not what the developer intended. We suppress the warning for - // casts to Bool and casts to optional types. Presumably such casts are not - // being performed for their optional-unwrapping behavior, but because the - // developer knows the type of the expression better than we do. - context.diagnose(.asExclamationMarkIsEvaluatedEarly(expr, in: macro)) - - default: - // Only diagnose for `x as! T`. `x as T` is perfectly fine if it otherwise - // compiles. For example, `#require(x as Int?)` should compile. - // - // If the token after "as" is something else entirely and got through the - // type checker, just leave it alone as we don't recognize it. - break + /// The node to treat as the root node when expanding expressions. + var effectiveRootNode: Syntax + + /// The name of the instance of `__ExpectationContext` to call. + var expressionContextNameExpr: DeclReferenceExprSyntax + + /// A list of any syntax nodes that have been rewritten. + /// + /// The nodes in this array are the _original_ nodes, not the rewritten nodes. + var rewrittenNodes = Set() + + /// Any postflight code the caller should insert into the closure containing + /// the rewritten syntax tree. + var teardownItems = [CodeBlockItemSyntax]() + + init(in context: C, for macro: M, rootedAt effectiveRootNode: Syntax, expressionContextName: TokenSyntax) { + self.context = context + self.macro = macro + self.effectiveRootNode = effectiveRootNode + self.expressionContextNameExpr = DeclReferenceExprSyntax(baseName: expressionContextName.trimmed) + super.init() } - return Condition(expression: expr) -} + /// The number of calls to `_rewrite()` made along the current node hierarchy. + /// + /// This value is incremented with each call to `_rewrite()` and managed by + /// `_visitChild()`. + private var _rewriteDepth = 0 -/// Parse a condition argument from a closure expression. -/// -/// - Parameters: -/// - expr: The closure expression. -/// - macro: The macro expression being expanded. -/// - context: The macro context in which the expression is being parsed. -/// -/// - Returns: An instance of ``Condition`` describing `expr`. -private func _parseCondition(from expr: ClosureExprSyntax, for macro: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> Condition { - if expr.signature == nil && expr.statements.count == 1, let item = expr.statements.first?.item { - // TODO: capture closures as a different kind of Testing.Expression with a - // separate subexpression per code item. + /// Rewrite a given syntax node by inserting a call to the expression context + /// (or rather, its `callAsFunction(_:_:)` member). + /// + /// - Parameters: + /// - node: The node to rewrite. + /// - originalNode: The original node in the original syntax tree, if `node` + /// has already been partially rewritten or substituted. If `node` has not + /// been rewritten, this argument should equal it. + /// - functionName: If not `nil`, the name of the function to call (as a + /// member function of the expression context.) + /// - additionalArguments: Any additional arguments to pass to the function. + /// + /// - Returns: A rewritten copy of `node` that calls into the expression + /// context when it is evaluated at runtime. + private func _rewrite( + _ node: @autoclosure () -> some ExprSyntaxProtocol, + originalWas originalNode: @autoclosure () -> some ExprSyntaxProtocol, + calling functionName: @autoclosure () -> TokenSyntax? = nil, + passing additionalArguments: @autoclosure () -> [Argument] = [] + ) -> ExprSyntax { + _rewriteDepth += 1 + if _rewriteDepth > _maximumRewriteDepth { + // At least 2 ancestors of this node have already been rewritten, so do + // not recursively rewrite further. This is necessary to limit the added + // exponentional complexity we're throwing at the type checker. + return ExprSyntax(originalNode()) + } + + // We're going to rewrite the node, so we'll evaluate the arguments now. + let node = node() + let originalNode = originalNode() + let functionName = functionName() + let additionalArguments = additionalArguments() + rewrittenNodes.insert(Syntax(originalNode)) - // If a closure contains a single statement or declaration, we can't - // meaningfully break it down as an expression, but we can still capture its - // source representation. - return Condition(expression: expr, expressionNode: Syntax(item)) + let calledExpr: ExprSyntax = if let functionName { + ExprSyntax(MemberAccessExprSyntax(base: expressionContextNameExpr, name: functionName)) + } else { + ExprSyntax(expressionContextNameExpr) + } + + var result = FunctionCallExprSyntax( + calledExpression: calledExpr, + leftParen: .leftParenToken(), + rightParen: .rightParenToken() + ) { + LabeledExprSyntax(expression: node.trimmed) + LabeledExprSyntax(expression: originalNode.expressionID(rootedAt: effectiveRootNode)) + for argument in additionalArguments { + LabeledExprSyntax(argument) + } + } + + result.leadingTrivia = originalNode.leadingTrivia + result.trailingTrivia = originalNode.trailingTrivia + + // If the resulting expression has an optional type due to containing an + // optional chaining expression (e.g. `foo?`) *and* its immediate parent + // node passes through the syntactical effects of optional chaining, return + // it as optional-chained so that it parses correctly post-expansion. + switch node.parent?.kind { + case .memberAccessExpr, .subscriptCallExpr: + let optionalChainFinder = _OptionalChainFinder(viewMode: .sourceAccurate) + optionalChainFinder.walk(node) + if optionalChainFinder.optionalChainFound { + return ExprSyntax(OptionalChainingExprSyntax(expression: result)) + } + + default: + break + } + + return ExprSyntax(result) } - return Condition(expression: expr) -} + /// Rewrite a given syntax node by inserting a call to the expression context + /// (or rather, its `callAsFunction(_:_:)` member). + /// + /// - Parameters: + /// - node: The node to rewrite. + /// - functionName: If not `nil`, the name of the function to call (as a + /// member function of the expression context.) + /// - additionalArguments: Any additional arguments to pass to the function. + /// + /// - Returns: A rewritten copy of `node` that calls into the expression + /// context when it is evaluated at runtime. + /// + /// This function is equivalent to `_rewrite(node, originalWas: node)`. + private func _rewrite(_ node: some ExprSyntaxProtocol, calling functionName: TokenSyntax? = nil, passing additionalArguments: [Argument] = []) -> ExprSyntax { + _rewrite(node, originalWas: node, calling: functionName, passing: additionalArguments) + } -/// A class that walks a syntax tree looking for optional chaining expressions -/// such as `a?.b.c`. -private final class _OptionalChainFinder: SyntaxVisitor { - /// Whether or not any optional chaining was found. - var optionalChainFound = false + /// Visit `node` as a child of another previously-visited node. + /// + /// - Parameters: + /// - node: The node to visit. + /// + /// - Returns: `node`, or a modified copy thereof if `node` or a child node + /// was rewritten. + /// + /// Use this function instead of calling `visit(_:)` or `rewrite(_:detach:)` + /// recursively. + /// + /// This overload simply visits `node` and is used for nodes that cannot be + /// rewritten directly (because they are not expressions.) + @_disfavoredOverload + private func _visitChild(_ node: S) -> S where S: SyntaxProtocol { + rewrite(node, detach: true).cast(S.self) + } - override func visit(_ node: OptionalChainingExprSyntax) -> SyntaxVisitorContinueKind { - optionalChainFound = true - return .skipChildren + /// Visit `node` as a child of another previously-visited node. + /// + /// - Parameters: + /// - node: The node to visit. + /// + /// - Returns: `node`, or a modified copy thereof if `node` or a child node + /// was rewritten. + /// + /// Use this function instead of calling `visit(_:)` or `rewrite(_:detach:)` + /// recursively. + private func _visitChild(_ node: some ExprSyntaxProtocol) -> ExprSyntax { + let oldRewriteDepth = _rewriteDepth + defer { + _rewriteDepth = oldRewriteDepth + } + + return rewrite(node, detach: true).cast(ExprSyntax.self) } -} -/// Extract the underlying expression from an optional-chained expression as -/// well as the number of question marks required to reach it. -/// -/// - Parameters: -/// - expr: The expression to examine, typically the `base` expression of a -/// `MemberAccessExprSyntax` instance. -/// -/// - Returns: A copy of `expr` with trailing question marks from optional -/// chaining removed, as well as a string containing the number of question -/// marks needed to access a member of `expr` after it has been assigned to -/// another variable. If `expr` does not contain any optional chaining, it is -/// returned verbatim along with the empty string. -/// -/// This function is used when expanding member accesses (either functions or -/// properties) that could contain optional chaining expressions such as -/// `foo?.bar()`. Since, in this case, `bar()` is ultimately going to be called -/// on a closure argument (i.e. `$0`), it is necessary to determine the number -/// of question mark characters needed to correctly construct that expression -/// and to capture the underlying expression of `foo?` without question marks so -/// that it remains syntactically correct when used without `bar()`. -private func _exprFromOptionalChainedExpr(_ expr: some ExprSyntaxProtocol) -> (ExprSyntax, questionMarks: String) { - let originalExpr = expr - var expr = ExprSyntax(expr) - var questionMarkCount = 0 - - while let optionalExpr = expr.as(OptionalChainingExprSyntax.self) { - // If the rightmost base expression is an optional-chained member access - // expression (e.g. "bar?" in the member access expression - // "foo.bar?.isQuux"), drop the question mark. - expr = optionalExpr.expression - questionMarkCount += 1 - } - - // If the rightmost expression is not itself optional-chained, check if any of - // the member accesses in the expression use optional chaining and, if one - // does, ensure we preserve optional chaining in the macro expansion. - if questionMarkCount == 0 { - let optionalChainFinder = _OptionalChainFinder(viewMode: .sourceAccurate) - optionalChainFinder.walk(originalExpr) - if optionalChainFinder.optionalChainFound { - questionMarkCount = 1 + /// Whether or not the parent node of the given node is capable of containing + /// a rewritten `DeclReferenceExprSyntax` instance. + /// + /// - Parameters: + /// - node: The node that might be rewritten. It does not need to be an + /// instance of `DeclReferenceExprSyntax`. + /// + /// - Returns: Whether or not the _parent_ of `node` will still be + /// syntactically valid if `node` is rewritten with `_rewrite(_:)`. + /// + /// Instances of `DeclReferenceExprSyntax` are often present in positions + /// where it would be syntactically invalid to extract them out as function + /// arguments. This function is used to constrain the cases where we do so to + /// those we know (or generally know) are "safe". + private func _isParentOfDeclReferenceExprValidForRewriting(_ node: some SyntaxProtocol) -> Bool { + guard let parentNode = node.parent else { + return false + } + + switch parentNode.kind { + case .labeledExpr, .functionParameter, + .prefixOperatorExpr, .postfixOperatorExpr, .infixOperatorExpr, + .asExpr, .isExpr, .optionalChainingExpr, .forceUnwrapExpr, + .arrayElement, .dictionaryElement: + return true + default: + return false } } - let questionMarks = String(repeating: "?", count: questionMarkCount) + override func visit(_ node: DeclReferenceExprSyntax) -> ExprSyntax { + // DeclReferenceExprSyntax is used for operator tokens in identifier + // position. These generally appear when an operator function is passed to + // a higher-order function (e.g. `sort(by: <)`) and also for the unbounded + // range expression (`...`). Both are uninteresting to the testing library + // and can be dropped. + if node.baseName.isOperator { + return ExprSyntax(node) + } - return (expr, questionMarks) -} + if _isParentOfDeclReferenceExprValidForRewriting(node) { + return _rewrite(node) + } -/// Parse a condition argument from a member function call. -/// -/// - Parameters: -/// - expr: The function call expression. -/// - macro: The macro expression being expanded. -/// - context: The macro context in which the expression is being parsed. -/// -/// - Returns: An instance of ``Condition`` describing `expr`. -private func _parseCondition(from expr: FunctionCallExprSyntax, for macro: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> Condition { - // We do not support function calls with trailing closures because the - // transform required to forward them requires more information than is - // available solely from the syntax tree. - if expr.trailingClosure != nil { - return Condition(expression: expr) - } - - // We also do not support expansion of closure invocations as they are - // diagnostically uninteresting. - if expr.calledExpression.is(ClosureExprSyntax.self) { - return Condition(expression: expr) - } - - let memberAccessExpr = expr.calledExpression.as(MemberAccessExprSyntax.self) - let functionName = memberAccessExpr.map(\.declName.baseName).map(Syntax.init) ?? Syntax(expr.calledExpression) - let argumentList = expr.arguments.map(Argument.init) - - let inOutArguments: [InOutExprSyntax] = argumentList.lazy - .map(\.expression) - .compactMap({ $0.as(InOutExprSyntax.self) }) - if inOutArguments.count > 1 { - // There is more than one inout argument present. This requires that the - // corresponding __check() function support variadic generics, but there is - // a compiler bug preventing us from implementing variadic inout support. - return Condition(expression: expr) - } else if inOutArguments.count != 0 && inOutArguments.count != argumentList.count { - // There is a mix of inout and normal arguments. That's not feasible for - // us to support here, so back out. - return Condition(expression: expr) - } - - // Which __check() function are we calling? - let expandedFunctionName = inOutArguments.isEmpty ? "__checkFunctionCall" : "__checkInoutFunctionCall" - - let indexedArguments = argumentList.lazy - .enumerated() - .map { index, argument in - if argument.expression.is(InOutExprSyntax.self) { - return Argument(label: argument.label, expression: "&$\(raw: index + 1)") - } - return Argument(label: argument.label, expression: "$\(raw: index + 1)") + // SPECIAL CASE: If this node is the base expression of a member access + // expression, and that member access expression is the called expression of + // a function, it is generally safe to extract out (but may need `.self` + // added to the end.) + // + // Module names are an exception to this rule as they cannot be referred to + // directly in source. So for instance, the following expression will be + // expanded incorrectly: + // + // #expect(Testing.foo(bar)) + // + // These sorts of expressions are relatively rare, so we'll allow the bug + // for the sake of better diagnostics in the common case. + if let memberAccessExpr = node.parent?.as(MemberAccessExprSyntax.self), + ExprSyntax(node) == memberAccessExpr.base, + let functionCallExpr = memberAccessExpr.parent?.as(FunctionCallExprSyntax.self), + ExprSyntax(memberAccessExpr) == functionCallExpr.calledExpression { + return _rewrite( + MemberAccessExprSyntax( + base: node.trimmed, + declName: DeclReferenceExprSyntax(baseName: .keyword(.self)) + ), + originalWas: node + ) } - let forwardedArguments = argumentList.lazy - .map(\.expression) - .map { Argument(expression: $0) } - - var baseExprForExpression: ExprSyntax? - var conditionArguments = [Argument]() - if let memberAccessExpr, var baseExpr = memberAccessExpr.base { - let questionMarks: String - (baseExpr, questionMarks) = _exprFromOptionalChainedExpr(baseExpr) - baseExprForExpression = baseExpr - - conditionArguments.append(Argument(expression: "\(baseExpr.trimmed).self")) // BUG: rdar://113152370 - conditionArguments.append( - Argument( - label: "calling", - expression: """ - { - $0\(raw: questionMarks).\(functionName.trimmed)(\(LabeledExprListSyntax(indexedArguments))) - } - """ + + return ExprSyntax(node) + } + + override func visit(_ node: TupleExprSyntax) -> ExprSyntax { + // We are conservative when descending into tuples because they could be + // tuple _types_ rather than _values_ (e.g. `(Int, Double)`) but those + // cannot be distinguished with syntax alone. + if _isParentOfDeclReferenceExprValidForRewriting(node) { + return _rewrite( + TupleExprSyntax { + for element in node.elements { + _visitChild(element).trimmed + } + }, + originalWas: node ) + } + + return ExprSyntax(node) + } + + override func visit(_ node: MemberAccessExprSyntax) -> ExprSyntax { + if case .keyword = node.declName.baseName.tokenKind { + // Likely something like Foo.self or X.Type, which we can't reasonably + // break down further. + return ExprSyntax(node) + } + + // As with decl reference expressions, only certain kinds of member access + // expressions can be directly extracted out. + if _isParentOfDeclReferenceExprValidForRewriting(node) { + return _rewrite( + node.with(\.base, node.base.map(_visitChild)), + originalWas: node + ) + } + + return ExprSyntax(node.with(\.base, node.base.map(_visitChild))) + } + + override func visit(_ node: FunctionCallExprSyntax) -> ExprSyntax { + _rewrite( + node + .with(\.calledExpression, _visitChild(node.calledExpression)) + .with(\.arguments, _visitChild(node.arguments)), + originalWas: node + ) + } + + override func visit(_ node: SubscriptCallExprSyntax) -> ExprSyntax { + _rewrite( + node + .with(\.calledExpression, _visitChild(node.calledExpression)) + .with(\.arguments, _visitChild(node.arguments)), + originalWas: node ) - } else { - // Substitute an empty tuple for the self argument, and call the function - // directly (without having to reorder the numbered closure arguments.) If - // the function takes zero arguments, we'll also need to suppress $0 in the - // closure body since it is unused. - let parameterList = forwardedArguments.isEmpty ? "_ in" : "" - conditionArguments.append(Argument(expression: "()")) - - // If memberAccessExpr is not nil here, that means it had a nil base - // expression (i.e. the base is inferred.) - var dot: TokenSyntax? - if memberAccessExpr != nil { - dot = .periodToken() + } + + override func visit(_ node: ClosureExprSyntax) -> ExprSyntax { + // We do not (currently) attempt to descend into closures. + ExprSyntax(node) + } + + override func visit(_ node: MacroExpansionExprSyntax) -> ExprSyntax { + // We do not attempt to descend into freestanding macros. + ExprSyntax(node) + } + + override func visit(_ node: FunctionDeclSyntax) -> DeclSyntax { + // We do not (currently) attempt to descend into functions. + DeclSyntax(node) + } + + // MARK: - Operators + + override func visit(_ node: PrefixOperatorExprSyntax) -> ExprSyntax { + // Special-case negative number literals as a single expression. + if node.expression.is(IntegerLiteralExprSyntax.self) || node.expression.is(FloatLiteralExprSyntax.self) { + if node.operator.tokenKind == .prefixOperator("-") { + return ExprSyntax(node) + } } - conditionArguments.append( - Argument( - label: "calling", - expression: """ - { \(raw: parameterList) - \(dot)\(functionName.trimmed)(\(LabeledExprListSyntax(indexedArguments))) - } - """ + return _rewrite( + node + .with(\.expression, _visitChild(node.expression)), + originalWas: node + ) + } + + override func visit(_ node: InfixOperatorExprSyntax) -> ExprSyntax { + if let op = node.operator.as(BinaryOperatorExprSyntax.self)?.operator.textWithoutBackticks, + op == "==" || op == "!=" || op == "===" || op == "!==" { + + return _rewrite( + ClosureExprSyntax { + InfixOperatorExprSyntax( + leftOperand: DeclReferenceExprSyntax(baseName: .dollarIdentifier("$0")) + .with(\.trailingTrivia, .space), + operator: BinaryOperatorExprSyntax(text: op), + rightOperand: DeclReferenceExprSyntax(baseName: .dollarIdentifier("$1")) + .with(\.leadingTrivia, .space) + ) + }, + originalWas: node, + calling: .identifier("__cmp"), + passing: [ + Argument(expression: _visitChild(node.leftOperand)), + Argument(expression: node.leftOperand.expressionID(rootedAt: effectiveRootNode)), + Argument(expression: _visitChild(node.rightOperand)), + Argument(expression: node.rightOperand.expressionID(rootedAt: effectiveRootNode)) + ] ) + } + + return _rewrite( + node + .with(\.leftOperand, _visitChild(node.leftOperand)) + .with(\.rightOperand, _visitChild(node.rightOperand)), + originalWas: node ) } - conditionArguments += forwardedArguments - return Condition( - expandedFunctionName, - arguments: conditionArguments, - expression: createExpressionExprForFunctionCall(baseExprForExpression, functionName, argumentList) - ) -} -/// Parse a condition argument from a property access. -/// -/// - Parameters: -/// - expr: The member access expression. -/// - macro: The macro expression being expanded. -/// - context: The macro context in which the expression is being parsed. -/// -/// - Returns: An instance of ``Condition`` describing `expr`. -private func _parseCondition(from expr: MemberAccessExprSyntax, for macro: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> Condition { - // Only handle member access expressions where the base expression is known - // and where there are no argument names (which would otherwise indicate a - // reference to a member function which wouldn't resolve to anything useful at - // runtime.) - guard var baseExpr = expr.base, expr.declName.argumentNames == nil else { - return Condition(expression: expr) - } - - let questionMarks: String - (baseExpr, questionMarks) = _exprFromOptionalChainedExpr(baseExpr) - - return Condition( - "__checkPropertyAccess", - arguments: [ - Argument(expression: "\(baseExpr.trimmed).self"), - Argument(label: "getting", expression: "{ $0\(raw: questionMarks).\(expr.declName.baseName) }") - ], - expression: createExpressionExprForPropertyAccess(baseExpr, expr.declName) - ) -} + override func visit(_ node: InOutExprSyntax) -> ExprSyntax { + // Swift's Law of Exclusivity means that only one subexpression in the + // expectation ought to be interacting with `value` when it is passed + // `inout`, so it should be sufficient to capture it in a `defer` statement + // that runs after the expression is evaluated. -/// Parse a condition argument from a property access. -/// -/// - Parameters: -/// - expr: The expression that was negated. -/// - isParenthetical: Whether or not `expression` was enclosed in -/// parentheses (and the `!` operator was outside it.) This argument -/// affects how this expression is represented as a string. -/// - macro: The macro expression being expanded. -/// - context: The macro context in which the expression is being parsed. -/// -/// - Returns: An instance of ``Condition`` describing `expr`. -private func _parseCondition(negating expr: ExprSyntax, isParenthetical: Bool, for macro: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> Condition { - var result = _parseCondition(from: expr, for: macro, in: context) - result.expression = createExpressionExprForNegation(of: result.expression, isParenthetical: isParenthetical) - return result -} + let rewrittenExpr = _rewrite(node.expression, calling: .identifier("__inoutAfter")) + if rewrittenExpr != ExprSyntax(node.expression) { + let teardownItem = CodeBlockItemSyntax(item: .expr(rewrittenExpr)) + teardownItems.append(teardownItem) + } -/// Parse a condition argument from an arbitrary expression. -/// -/// - Parameters: -/// - expr: The condition expression to parse. -/// - macro: The macro expression being expanded. -/// - context: The macro context in which the expression is being parsed. -/// -/// - Returns: An instance of ``Condition`` describing `expr`. -private func _parseCondition(from expr: ExprSyntax, for macro: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> Condition { - // Handle closures with a single expression in them (e.g. { $0.foo() }) - if let closureExpr = expr.as(ClosureExprSyntax.self) { - return _parseCondition(from: closureExpr, for: macro, in: context) + // The argument should not be expanded in-place as we can't return an + // argument passed `inout` and expect it to remain semantically correct. + return ExprSyntax(node) + } + + // MARK: - Casts + + /// Rewrite an `is` or `as?` cast. + /// + /// - Parameters: + /// - valueExpr: The expression to cast. + /// - isAsKeyword: The casting keyword (either `.is` or `.as`). + /// - type: The type to cast `valueExpr` to. + /// - originalNode: The original `IsExprSyntax` or `AsExprSyntax` node in + /// the original syntax tree. + /// + /// - Returns: A function call expression equivalent to the described cast. + private func _rewriteAsCast(_ valueExpr: ExprSyntax, _ isAsKeyword: Keyword, _ type: TypeSyntax, originalWas originalNode: some ExprSyntaxProtocol) -> ExprSyntax { + rewrittenNodes.insert(Syntax(type)) + + return _rewrite( + _visitChild(valueExpr).trimmed, + originalWas: originalNode, + calling: .identifier("__\(isAsKeyword)"), + passing: [ + Argument( + expression: MemberAccessExprSyntax( + base: TupleExprSyntax { + LabeledExprSyntax(expression: TypeExprSyntax(type: type.trimmed)) + }, + declName: DeclReferenceExprSyntax(baseName: .keyword(.self)) + ) + ), + Argument(expression: type.expressionID(rootedAt: effectiveRootNode)) + ] + ) } - // If the condition involves the `try` or `await` keywords, assume we cannot - // expand it. This check cannot handle expressions like - // `try #expect(a.b(c))` where `b()` is throwing because the `try` keyword is - // outside the macro expansion. SEE: rdar://109470248 - let containsTryOrAwait = expr.tokens(viewMode: .sourceAccurate).lazy - .map(\.tokenKind) - .contains { $0 == .keyword(.try) || $0 == .keyword(.await) } - if containsTryOrAwait { - return Condition(expression: expr) + override func visit(_ node: AsExprSyntax) -> ExprSyntax { + switch node.questionOrExclamationMark?.tokenKind { + case .postfixQuestionMark: + return _rewriteAsCast(node.expression, .as, node.type, originalWas: node) + + case .exclamationMark where !node.type.isNamed("Bool", inModuleNamed: "Swift") && !node.type.isOptional: + // Warn that as! will be evaluated before #expect() or #require(), which is + // probably not what the developer intended. We suppress the warning for + // casts to Bool and casts to optional types. Presumably such casts are not + // being performed for their optional-unwrapping behavior, but because the + // developer knows the type of the expression better than we do. + context.diagnose(.asExclamationMarkIsEvaluatedEarly(node, in: macro)) + return _rewrite(node) + + case .exclamationMark: + // Only diagnose for `x as! T`. `x as T` is perfectly fine if it otherwise + // compiles. For example, `#require(x as Int?)` should compile. + return _rewrite(node) + + default: + // This is an "escape hatch" cast. Do not attempt to process the cast. + return ExprSyntax(node) + } } - if let infixOperator = expr.as(InfixOperatorExprSyntax.self), - let op = infixOperator.operator.as(BinaryOperatorExprSyntax.self) { - return _parseCondition(from: expr, leftOperand: infixOperator.leftOperand, operator: op, rightOperand: infixOperator.rightOperand, for: macro, in: context) + override func visit(_ node: IsExprSyntax) -> ExprSyntax { + _rewriteAsCast(node.expression, .is, node.type, originalWas: node) } - // Handle `is` and `as?` expressions. - if let isExpr = expr.as(IsExprSyntax.self) { - return _parseCondition(from: isExpr, for: macro, in: context) - } else if let asExpr = expr.as(AsExprSyntax.self) { - return _parseCondition(from: asExpr, for: macro, in: context) + // MARK: - Literals + + override func visit(_ node: BooleanLiteralExprSyntax) -> ExprSyntax { + // Contrary to the comment immediately below this function, we *do* rewrite + // boolean literals so that expressions like `#expect(true)` are expanded. + _rewrite(node) } - // Handle function calls and member accesses. - if let functionCallExpr = expr.as(FunctionCallExprSyntax.self) { - return _parseCondition(from: functionCallExpr, for: macro, in: context) - } else if let memberAccessExpr = expr.as(MemberAccessExprSyntax.self) { - return _parseCondition(from: memberAccessExpr, for: macro, in: context) + // We don't currently rewrite numeric/string/array/dictionary literals. We + // could, but it's unclear what the benefit would be and it could seriously + // impact type checker time. + +#if SWT_DELVE_INTO_LITERALS + override func visit(_ node: IntegerLiteralExprSyntax) -> ExprSyntax { + _rewrite(node) } - // Handle negation. - if let negatedExpr = _negatedExpression(expr) { - return _parseCondition(negating: negatedExpr.0, isParenthetical: negatedExpr.isParenthetical, for: macro, in: context) + override func visit(_ node: FloatLiteralExprSyntax) -> ExprSyntax { + _rewrite(node) } - // Parentheses are parsed as if they were tuples, so (true && false) appears - // to the parser as a tuple containing one expression, `true && false`. - if let expr = removeParentheses(from: expr) { - return _parseCondition(from: expr, for: macro, in: context) + override func visit(_ node: StringLiteralExprSyntax) -> ExprSyntax { + _rewrite(node) } - return Condition(expression: expr) + override func visit(_ node: ArrayExprSyntax) -> ExprSyntax { + _rewrite( + node.with( + \.elements, ArrayElementListSyntax { + for element in node.elements { + ArrayElementSyntax(expression: _visitChild(element.expression).trimmed) + } + } + ), + originalWas: node + ) + } + + override func visit(_ node: DictionaryExprSyntax) -> ExprSyntax { + guard case let .elements(elements) = node.content else { + return ExprSyntax(node) + } + return _rewrite( + node.with( + \.content, .elements( + DictionaryElementListSyntax { + for element in elements { + DictionaryElementSyntax(key: _visitChild(element.key).trimmed, value: _visitChild(element.value).trimmed) + } + } + ) + ), + originalWas: node + ) + } +#else + override func visit(_ node: ArrayExprSyntax) -> ExprSyntax { + return ExprSyntax(node) + } + + override func visit(_ node: DictionaryExprSyntax) -> ExprSyntax { + return ExprSyntax(node) + } +#endif +} + +extension ConditionMacro { + /// Rewrite and expand upon an expression node. + /// + /// - Parameters: + /// - node: The root of a syntax tree to rewrite. This node may not itself + /// be the root of the overall syntax tree—it's just the root of the + /// subtree that we're rewriting. + /// - expressionContextName: The name of the instance of + /// `__ExpectationContext` to call at runtime. + /// - macro: The macro expression. + /// - effectiveRootNode: The node to treat as the root of the syntax tree + /// for the purposes of generating expression ID values. + /// - effectKeywordsToApply: The set of effect keywords in the expanded + /// expression or its lexical context that may apply to `node`. + /// - returnType: The return type of the expanded closure, if statically + /// known at macro expansion time. + /// - context: The macro context in which the expression is being parsed. + /// + /// - Returns: A tuple containing the rewritten copy of `node`, a list of all + /// the nodes within `node` (possibly including `node` itself) that were + /// rewritten, and a code block containing code that should be inserted into + /// the lexical scope of `node` _before_ its rewritten equivalent. + static func rewrite( + _ node: some ExprSyntaxProtocol, + usingExpressionContextNamed expressionContextName: TokenSyntax, + for macro: some FreestandingMacroExpansionSyntax, + rootedAt effectiveRootNode: some SyntaxProtocol, + effectKeywordsToApply: Set, + returnType: (some TypeSyntaxProtocol)?, + in context: some MacroExpansionContext + ) -> (ClosureExprSyntax, rewrittenNodes: Set) { + _diagnoseTrivialBooleanValue(from: ExprSyntax(node), for: macro, in: context) + + let contextInserter = _ContextInserter(in: context, for: macro, rootedAt: Syntax(effectiveRootNode), expressionContextName: expressionContextName) + var expandedExpr = contextInserter.rewrite(node, detach: true).cast(ExprSyntax.self) + let rewrittenNodes = contextInserter.rewrittenNodes + + // Insert additional effect keywords/thunks as needed. + var effectKeywordsToApply = effectKeywordsToApply + if isThrowing { + effectKeywordsToApply.insert(.try) + } + expandedExpr = applyEffectfulKeywords(effectKeywordsToApply, to: expandedExpr) + + // Construct the body of the closure that we'll pass to the expanded + // function. + var codeBlockItems = CodeBlockItemListSyntax { + if contextInserter.teardownItems.isEmpty { + expandedExpr.with(\.trailingTrivia, .newline) + } else { + // Insert a defer statement that runs any teardown items. + DeferStmtSyntax { + for teardownItem in contextInserter.teardownItems { + teardownItem.with(\.trailingTrivia, .newline) + } + }.with(\.trailingTrivia, .newline) + + // If we're inserting any additional code into the closure before + // the rewritten argument, we can't elide the return keyword. + ReturnStmtSyntax( + expression: expandedExpr.with(\.leadingTrivia, .space) + ).with(\.trailingTrivia, .newline) + } + } + + // Replace any dollar identifiers in the code block, then construct a + // capture list for the closure (if needed.) + var captureList: ClosureCaptureClauseSyntax? + do { + let dollarIDReplacer = _DollarIdentifierReplacer() + codeBlockItems = dollarIDReplacer.rewrite(codeBlockItems, detach: true).cast(CodeBlockItemListSyntax.self) + if !dollarIDReplacer.dollarIdentifierTokenKinds.isEmpty { + let dollarIdentifierTokens = dollarIDReplacer.dollarIdentifierTokenKinds.map { tokenKind in + TokenSyntax(tokenKind, presence: .present) + } + captureList = ClosureCaptureClauseSyntax { + for token in dollarIdentifierTokens { + ClosureCaptureSyntax( + name: _rewriteDollarIdentifier(token), + initializer: InitializerClauseSyntax( + value: DeclReferenceExprSyntax(baseName: token) + ) + ) + } + } + } + } + + // Enclose the code block in the final closure. + let closureExpr = ClosureExprSyntax( + signature: ClosureSignatureSyntax( + capture: captureList, + parameterClause: .parameterClause( + ClosureParameterClauseSyntax( + parameters: ClosureParameterListSyntax { + ClosureParameterSyntax( + firstName: expressionContextName, + colon: .colonToken().with(\.trailingTrivia, .space), + type: TypeSyntax( + AttributedTypeSyntax( + specifiers: [ + TypeSpecifierListSyntax.Element( + SimpleTypeSpecifierSyntax(specifier: .keyword(.inout)) + .with(\.trailingTrivia, .space) + ) + ], + baseType: MemberTypeSyntax( + baseType: IdentifierTypeSyntax(name: .identifier("Testing")), + name: .identifier("__ExpectationContext") + ) + ) + ) + ) + } + ) + ), + returnClause: returnType.map { returnType in + ReturnClauseSyntax( + type: returnType.with(\.leadingTrivia, .space) + ).with(\.leadingTrivia, .space) + }, + inKeyword: .keyword(.in) + .with(\.leadingTrivia, .space) + .with(\.trailingTrivia, .newline) + ), + statements: codeBlockItems + ) + + return (closureExpr, rewrittenNodes) + } +} + +// MARK: - Finding optional chains + +/// A class that walks a syntax tree looking for optional chaining expressions +/// such as `a?.b.c`. +private final class _OptionalChainFinder: SyntaxVisitor { + /// Whether or not any optional chaining was found. + var optionalChainFound = false + + override func visit(_ node: OptionalChainingExprSyntax) -> SyntaxVisitorContinueKind { + optionalChainFound = true + return .skipChildren + } } -// MARK: - +// MARK: - Replacing dollar identifiers -/// Parse a condition argument from an arbitrary expression. +/// Rewrite a dollar identifier as a normal (non-dollar) identifier. /// /// - Parameters: -/// - expr: The condition expression to parse. -/// - macro: The macro expression being expanded. -/// - context: The macro context in which the expression is being parsed. +/// - token: The dollar identifier token to rewrite. /// -/// - Returns: An instance of ``Condition`` describing `expr`. -func parseCondition(from expr: ExprSyntax, for macro: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> Condition { - _diagnoseTrivialBooleanValue(from: expr, for: macro, in: context) - let result = _parseCondition(from: expr, for: macro, in: context) +/// - Returns: A copy of `token` as an identifier token. +private func _rewriteDollarIdentifier(_ token: TokenSyntax) -> TokenSyntax { + var result = TokenSyntax.identifier("__renamedCapture__\(token.trimmedDescription)") + + result.leadingTrivia = token.leadingTrivia + result.trailingTrivia = token.trailingTrivia + return result } + +/// A syntax rewriter that replaces _numeric_ dollar identifiers (e.g. `$0`) +/// with normal (non-dollar) identifiers. +private final class _DollarIdentifierReplacer: SyntaxRewriter { + /// The `tokenKind` properties of any dollar identifier tokens that have been + /// rewritten. + var dollarIdentifierTokenKinds = Set() + + override func visit(_ node: TokenSyntax) -> TokenSyntax { + if case let .dollarIdentifier(id) = node.tokenKind, id.dropFirst().allSatisfy(\.isWholeNumber) { + // This dollar identifier is numeric, so it's a closure argument. + dollarIdentifierTokenKinds.insert(node.tokenKind) + return _rewriteDollarIdentifier(node) + } + + return node + } + + override func visit(_ node: ClosureExprSyntax) -> ExprSyntax { + // Do not recurse into closure expressions because they will have their own + // argument lists that won't conflict with the enclosing scope's. + return ExprSyntax(node) + } +} + +// MARK: - Source code capturing + +/// Create a dictionary literal expression containing the source code +/// representations of a set of syntax nodes. +/// +/// - Parameters: +/// - nodes: The nodes whose source code should be included in the resulting +/// dictionary literal. +/// - effectiveRootNode: The node to treat as the root of the syntax tree +/// for the purposes of generating expression ID values. +/// +/// - Returns: A dictionary literal expression whose keys are expression IDs and +/// whose values are string literals containing the source code of the syntax +/// nodes in `nodes`. +func createDictionaryExpr(forSourceCodeOf nodes: some Sequence, rootedAt effectiveRootNode: some SyntaxProtocol) -> DictionaryExprSyntax { + // Sort the nodes. This isn't strictly necessary for correctness but it does + // make the produced code more consistent. + let nodes = nodes.sorted { $0.id < $1.id } + + return DictionaryExprSyntax { + for node in nodes { + DictionaryElementSyntax( + key: node.expressionID(rootedAt: effectiveRootNode), + value: StringLiteralExprSyntax(content: node.trimmedDescription) + ) + } + } +} + +/// Create a dictionary literal expression containing the source code +/// representations of a single syntax node. +/// +/// - Parameters: +/// - node: The nodes whose source code should be included in the resulting +/// dictionary literal. This node is treated as the root node. +/// +/// - Returns: A dictionary literal expression containing one key/value pair +/// where the key is the expression ID of `node` and the value is its source +/// code. +func createDictionaryExpr(forSourceCodeOf node: some SyntaxProtocol) -> DictionaryExprSyntax { + createDictionaryExpr(forSourceCodeOf: CollectionOfOne(node), rootedAt: node) +} diff --git a/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift b/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift new file mode 100644 index 000000000..ca5a752b7 --- /dev/null +++ b/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift @@ -0,0 +1,139 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +import SwiftSyntax + +// MARK: - Finding effect keywords + +/// A syntax visitor class that looks for effectful keywords in a given +/// expression. +private final class _EffectFinder: SyntaxAnyVisitor { + /// The effect keywords discovered so far. + var effectKeywords: Set = [] + + override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind { + switch node.kind { + case .tryExpr: + effectKeywords.insert(.try) + case .awaitExpr: + effectKeywords.insert(.await) + case .consumeExpr: + effectKeywords.insert(.consume) + case .closureExpr, .functionDecl: + // Do not delve into closures or function declarations. + return .skipChildren + case .variableDecl: + // Delve into variable declarations. + return .visitChildren + default: + // Do not delve into declarations other than variables. + if node.isProtocol((any DeclSyntaxProtocol).self) { + return .skipChildren + } + } + + // Recurse into everything else. + return .visitChildren + } +} + +/// Find effectful keywords in a syntax node. +/// +/// - Parameters: +/// - node: The node to inspect. +/// +/// - Returns: A set of effectful keywords such as `await` that are present in +/// `node`. +/// +/// This function does not descend into function declarations or closure +/// expressions because they represent distinct lexical contexts and their +/// effects are uninteresting in the context of `node` unless they are called. +func findEffectKeywords(in node: some SyntaxProtocol) -> Set { + let effectFinder = _EffectFinder(viewMode: .sourceAccurate) + effectFinder.walk(node) + return effectFinder.effectKeywords +} + +// MARK: - Inserting effect keywords/thunks + +/// Make a function call expression to an effectful thunk function provided by +/// the testing library. +/// +/// - Parameters: +/// - thunkName: The unqualified name of the thunk function to call. This +/// token must be the name of a function in the `Testing` module. +/// - expr: The expression to thunk. +/// +/// - Returns: An expression representing a call to the function named +/// `thunkName`, passing `expr`. +private func _makeCallToEffectfulThunk(_ thunkName: TokenSyntax, passing expr: some ExprSyntaxProtocol) -> ExprSyntax { + ExprSyntax( + FunctionCallExprSyntax( + calledExpression: MemberAccessExprSyntax( + base: DeclReferenceExprSyntax(baseName: .identifier("Testing")), + declName: DeclReferenceExprSyntax(baseName: thunkName) + ), + leftParen: .leftParenToken(), + rightParen: .rightParenToken() + ) { + LabeledExprSyntax(expression: expr.trimmed) + } + ) +} + +/// Apply the given effectful keywords (i.e. `try` and `await`) to an expression +/// using thunk functions provided by the testing library. +/// +/// - Parameters: +/// - effectfulKeywords: The effectful keywords to apply. +/// - expr: The expression to apply the keywords and thunk functions to. +/// +/// - Returns: A copy of `expr` if no changes are needed, or an expression that +/// adds the keywords in `effectfulKeywords` to `expr`. +func applyEffectfulKeywords(_ effectfulKeywords: Set, to expr: some ExprSyntaxProtocol) -> ExprSyntax { + let originalExpr = expr + var expr = ExprSyntax(expr) + + let needAwait = effectfulKeywords.contains(.await) && !expr.is(AwaitExprSyntax.self) + let needTry = effectfulKeywords.contains(.try) && !expr.is(TryExprSyntax.self) + + // First, add thunk function calls. + if needAwait { + expr = _makeCallToEffectfulThunk(.identifier("__requiringAwait"), passing: expr) + } + if needTry { + expr = _makeCallToEffectfulThunk(.identifier("__requiringTry"), passing: expr) + } + + // Then add keyword expressions. (We do this separately so we end up writing + // `try await __r(__r(self))` instead of `try __r(await __r(self))` which + // is less accepted by the compiler.) + if needAwait { + expr = ExprSyntax( + AwaitExprSyntax( + awaitKeyword: .keyword(.await).with(\.trailingTrivia, .space), + expression: expr + ) + ) + } + if needTry { + expr = ExprSyntax( + TryExprSyntax( + tryKeyword: .keyword(.try).with(\.trailingTrivia, .space), + expression: expr + ) + ) + } + + expr.leadingTrivia = originalExpr.leadingTrivia + expr.trailingTrivia = originalExpr.trailingTrivia + + return expr +} diff --git a/Sources/TestingMacros/Support/SourceCodeCapturing.swift b/Sources/TestingMacros/Support/SourceCodeCapturing.swift deleted file mode 100644 index 9fd687e8f..000000000 --- a/Sources/TestingMacros/Support/SourceCodeCapturing.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2023 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for Swift project authors -// - -import SwiftSyntax - -/// Get a swift-syntax expression initializing an instance of `__Expression` -/// from an arbitrary syntax node. -/// -/// - Parameters: -/// - node: A syntax node from which to construct an instance of -/// `__Expression`. -/// -/// - Returns: An expression value that initializes an instance of -/// `__Expression` for the specified syntax node. -func createExpressionExpr(from node: any SyntaxProtocol) -> ExprSyntax { - if let stringLiteralExpr = node.as(StringLiteralExprSyntax.self), - let stringValue = stringLiteralExpr.representedLiteralValue { - return ".__fromStringLiteral(\(literal: node.trimmedDescription), \(literal: stringValue))" - } - return ".__fromSyntaxNode(\(literal: node.trimmedDescription))" -} - -/// Get a swift-syntax expression initializing an instance of `__Expression` -/// from an arbitrary sequence of syntax nodes representing a binary operation. -/// -/// - Parameters: -/// - lhs: The left-hand operand. -/// - operator: The operator. -/// - rhs: The right-hand operand. -/// -/// - Returns: An expression value that initializes an instance of -/// `__Expression` for the specified syntax nodes. -func createExpressionExprForBinaryOperation(_ lhs: some SyntaxProtocol, _ `operator`: some SyntaxProtocol, _ rhs: some SyntaxProtocol) -> ExprSyntax { - let arguments = LabeledExprListSyntax { - LabeledExprSyntax(expression: createExpressionExpr(from: lhs)) - LabeledExprSyntax(expression: StringLiteralExprSyntax(content: `operator`.trimmedDescription)) - LabeledExprSyntax(expression: createExpressionExpr(from: rhs)) - } - - return ".__fromBinaryOperation(\(arguments))" -} - -/// Get a swift-syntax expression initializing an instance of `__Expression` -/// from an arbitrary sequence of syntax nodes representing a function call. -/// -/// - Parameters: -/// - value: The value on which the member function is being called, if any. -/// - functionName: The name of the member function being called. -/// - arguments: The arguments to the member function. -/// -/// - Returns: An expression value that initializes an instance of -/// `__Expression` for the specified syntax nodes. -func createExpressionExprForFunctionCall(_ value: (any SyntaxProtocol)?, _ functionName: some SyntaxProtocol, _ arguments: some Sequence) -> ExprSyntax { - let arguments = LabeledExprListSyntax { - if let value { - LabeledExprSyntax(expression: createExpressionExpr(from: value)) - } else { - LabeledExprSyntax(expression: NilLiteralExprSyntax()) - } - LabeledExprSyntax(expression: StringLiteralExprSyntax(content: functionName.trimmedDescription)) - for argument in arguments { - LabeledExprSyntax(expression: TupleExprSyntax { - if let argumentLabel = argument.label { - LabeledExprSyntax(expression: StringLiteralExprSyntax(content: argumentLabel.trimmedDescription)) - } else { - LabeledExprSyntax(expression: NilLiteralExprSyntax()) - } - LabeledExprSyntax(expression: createExpressionExpr(from: argument.expression)) - }) - } - } - - return ".__fromFunctionCall(\(arguments))" -} - -/// Get a swift-syntax expression initializing an instance of `__Expression` -/// from an arbitrary sequence of syntax nodes representing a property access. -/// -/// - Parameters: -/// - value: The value on which the property is being accessed, if any. -/// - keyPath: The name of the property being accessed. -/// -/// - Returns: An expression value that initializes an instance of -/// `__Expression` for the specified syntax nodes. -func createExpressionExprForPropertyAccess(_ value: ExprSyntax, _ keyPath: DeclReferenceExprSyntax) -> ExprSyntax { - let arguments = LabeledExprListSyntax { - LabeledExprSyntax(expression: createExpressionExpr(from: value)) - LabeledExprSyntax(expression: createExpressionExpr(from: keyPath.baseName)) - } - - return ".__fromPropertyAccess(\(arguments))" -} - -/// Get a swift-syntax expression initializing an instance of `__Expression` -/// from an arbitrary sequence of syntax nodes representing the negation of -/// another expression. -/// -/// - Parameters: -/// - expression: An expression representing a previously-initialized instance -/// of `__Expression` (that is, not the expression in source, but the result -/// of a call to ``createExpressionExpr(from:)`` etc.) -/// - isParenthetical: Whether or not `expression` was enclosed in -/// parentheses (and the `!` operator was outside it.) This argument -/// affects how this expression is represented as a string. -/// -/// - Returns: An expression value that initializes an instance of -/// `__Expression` for the specified syntax nodes. -func createExpressionExprForNegation(of expression: ExprSyntax, isParenthetical: Bool) -> ExprSyntax { - ".__fromNegation(\(expression.trimmed), \(literal: isParenthetical))" -} diff --git a/Sources/TestingMacros/TestDeclarationMacro.swift b/Sources/TestingMacros/TestDeclarationMacro.swift index 1a3f2c448..ca5624ace 100644 --- a/Sources/TestingMacros/TestDeclarationMacro.swift +++ b/Sources/TestingMacros/TestDeclarationMacro.swift @@ -235,17 +235,17 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { // detecting isolation to other global actors. lazy var isMainActorIsolated = !functionDecl.attributes(named: "MainActor", inModuleNamed: "_Concurrency").isEmpty var forwardCall: (ExprSyntax) -> ExprSyntax = { - "try await Testing.__requiringTry(Testing.__requiringAwait(\($0)))" + applyEffectfulKeywords([.try, .await], to: $0) } let forwardInit = forwardCall if functionDecl.noasyncAttribute != nil { if isMainActorIsolated { forwardCall = { - "try await MainActor.run { try Testing.__requiringTry(\($0)) }" + "try await MainActor.run { \(applyEffectfulKeywords([.try], to: $0)) }" } } else { forwardCall = { - "try { try Testing.__requiringTry(\($0)) }()" + "try { \(applyEffectfulKeywords([.try], to: $0)) }()" } } } diff --git a/Sources/TestingMacros/TestingMacrosMain.swift b/Sources/TestingMacros/TestingMacrosMain.swift index 1894f4282..f330e0d3f 100644 --- a/Sources/TestingMacros/TestingMacrosMain.swift +++ b/Sources/TestingMacros/TestingMacrosMain.swift @@ -22,6 +22,7 @@ struct TestingMacrosMain: CompilerPlugin { TestDeclarationMacro.self, ExpectMacro.self, RequireMacro.self, + UnwrapMacro.self, AmbiguousRequireMacro.self, NonOptionalRequireMacro.self, RequireThrowsMacro.self, diff --git a/Sources/_TestingInternals/include/TestSupport.h b/Sources/_TestingInternals/include/TestSupport.h index 37d42692e..c0f913ba8 100644 --- a/Sources/_TestingInternals/include/TestSupport.h +++ b/Sources/_TestingInternals/include/TestSupport.h @@ -37,6 +37,10 @@ static inline bool swt_pointersNotEqual4(const char *a, const char *b, const cha return a != b && b != c && c != d; } +static inline bool swt_nullableCString(const char *_Nullable string) { + return string != 0; +} + SWT_ASSUME_NONNULL_END #endif diff --git a/Tests/SubexpressionShowcase/SubexpressionShowcase.swift b/Tests/SubexpressionShowcase/SubexpressionShowcase.swift new file mode 100644 index 000000000..fdaee6486 --- /dev/null +++ b/Tests/SubexpressionShowcase/SubexpressionShowcase.swift @@ -0,0 +1,118 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +import Testing + +#warning("TODO: convert this scratchpad into actual unit tests") + +func f(_ x: Int, _ y: Int) -> Int { + x + y +} + +func g() throws -> Int { + 22 +} + +func io(_ x: inout Int) -> Int { + x += 1 + return x + 1 +} + +struct T { + func h(_ i: Int) -> Bool { false } + static func j(_ d: Double) -> Bool { false } +} + +@Test func runSubexpressionShowcase() async { + await withKnownIssue { + try await subexpressionShowcase() + } +} + +func subexpressionShowcase() async throws { + #expect(false || true) + #expect((Int)(123) == 124) + #expect((Int, Double)(123, 456.0) == (124, 457.0)) + #expect((123, 456) == (789, 0x12)) + #expect((try g() > 500) && true) + + do { + let n = Int.random(in: 0 ..< 100) + var m = n + #expect(io(&m) == n) + } + + let closure: (Int) -> Void = { + #expect((($0 + $0 + $0) as Int) == 0x10) + } + closure(11) + + struct S: ~Copyable { + borrowing func h() -> Bool { false } + consuming func j() -> Bool { false } + } +#if false + // Unsupported: move-only types have too many constraints that cannot be + // resolved by inspecting syntax. Borrowing calls cannot be boxed (at least + // until we get @lifetime) and the compiler forbids making consuming calls in + // a closure in case the closure gets called multiple times. + // + // A workaround is to explicitly write `consume` on an expression (or the + // unsupported `_borrow`) which will tell our macros code not to try expanding + // the expression. + let s = S() + #expect(s.h()) + #expect(s.j()) // consuming -- this DOES still fail, no syntax-level way to tell +#endif + + let s2 = S() + _ = try #require(.some(consume s2)) + + let t = T() + #expect(t.h(.random(in: 0 ..< 100))) + #expect(T.j(.greatestFiniteMagnitude)) + #expect(SubexpressionShowcase.T.j(.greatestFiniteMagnitude)) + + + let x = 9 + let y = 11 + #expect(x == y) + #expect(try f(x, y) == g()) + + let z: Int? = nil + let zDelta = 1 + #expect(z?.advanced(by: zDelta) != nil) + + let v: String? = nil + #expect(v?[...] != nil) + #expect(v?[...].first != nil) + + func k(_ x: @autoclosure () -> Bool) async -> Bool { + x() + } + #expect(await k(true)) + +#if false + // Unsupported: __ec is necessarily inout and captures non-sendable state, so + // this will fail to compile. Making __ec a class instead is possible, but + // adds a very large amount of code and locking overhead for what we can + // assume is an edge case. + func m(_ x: @autoclosure @Sendable () -> Bool) -> Bool { + x() + } + #expect(m(123 == 456)) +#endif + + try #require(x == x) + _ = try #require(.some(Int32.Magnitude(1))) + + let n = 1 as Any + _ = try #require(n as? String) +} diff --git a/Tests/TestingMacrosTests/ConditionMacroTests.swift b/Tests/TestingMacrosTests/ConditionMacroTests.swift index 9f1201367..f21e5bd8a 100644 --- a/Tests/TestingMacrosTests/ConditionMacroTests.swift +++ b/Tests/TestingMacrosTests/ConditionMacroTests.swift @@ -23,183 +23,196 @@ struct ConditionMacroTests { @Test("#expect() macro", arguments: [ ##"#expect(true)"##: - ##"Testing.__checkValue(true, expression: .__fromSyntaxNode("true"), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in __ec(true, 0x0) }, sourceCode: [0x0: "true"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(false)"##: - ##"Testing.__checkValue(false, expression: .__fromSyntaxNode("false"), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in __ec(false, 0x0) }, sourceCode: [0x0: "false"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(false, "Custom message")"##: - ##"Testing.__checkValue(false, expression: .__fromSyntaxNode("false"), comments: ["Custom message"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in __ec(false, 0x0) }, sourceCode: [0x0: "false"], comments: ["Custom message"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(2 > 1)"##: - ##"Testing.__checkBinaryOperation(2, { $0 > $1() }, 1, expression: .__fromBinaryOperation(.__fromSyntaxNode("2"), ">", .__fromSyntaxNode("1")), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in __ec(2 > 1, 0x0) }, sourceCode: [0x0: "2 > 1"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(((true || false) && true) || Bool.random())"##: - ##"Testing.__checkBinaryOperation(((true || false) && true), { $0 || $1() }, Bool.random(), expression: .__fromBinaryOperation(.__fromSyntaxNode("((true || false) && true)"), "||", .__fromSyntaxNode("Bool.random()")), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in __ec(__ec((__ec(__ec((__ec(__ec(true, 0xf7a) || __ec(false, 0x877a), 0x77a)), 0x7a) && __ec(true, 0x10003a), 0x3a)), 0x2) || __ec(__ec(Bool.self, 0xe000000).random(), 0x2000000), 0x0) }, sourceCode: [0x0: "((true || false) && true) || Bool.random()", 0x2: "((true || false) && true)", 0x3a: "(true || false) && true", 0x7a: "(true || false)", 0x77a: "true || false", 0xf7a: "true", 0x877a: "false", 0x10003a: "true", 0x2000000: "Bool.random()", 0xe000000: "Bool"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(9 > 8 && 7 > 6, "Some comment")"##: - ##"Testing.__checkBinaryOperation(9 > 8, { $0 && $1() }, 7 > 6, expression: .__fromBinaryOperation(.__fromSyntaxNode("9 > 8"), "&&", .__fromSyntaxNode("7 > 6")), comments: ["Some comment"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in __ec(__ec(9 > 8, 0x2) && __ec(7 > 6, 0x400), 0x0) }, sourceCode: [0x0: "9 > 8 && 7 > 6", 0x2: "9 > 8", 0x400: "7 > 6"], comments: ["Some comment"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect("a" == "b")"##: - ##"Testing.__checkBinaryOperation("a", { $0 == $1() }, "b", expression: .__fromBinaryOperation(.__fromStringLiteral(#""a""#, "a"), "==", .__fromStringLiteral(#""b""#, "b")), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in __ec.__cmp({ $0 == $1 }, 0x0, "a", 0x2, "b", 0x200) }, sourceCode: [0x0: #""a" == "b""#], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(!Bool.random())"##: - ##"Testing.__checkFunctionCall(Bool.self, calling: { $0.random() }, expression: .__fromNegation(.__fromFunctionCall(.__fromSyntaxNode("Bool"), "random"), false), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in __ec(!__ec(__ec(Bool.self, 0x1c).random(), 0x4), 0x0) }, sourceCode: [0x0: "!Bool.random()", 0x4: "Bool.random()", 0x1c: "Bool"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect((true && false))"##: - ##"Testing.__checkBinaryOperation(true, { $0 && $1() }, false, expression: .__fromBinaryOperation(.__fromSyntaxNode("true"), "&&", .__fromSyntaxNode("false")), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in __ec((__ec(__ec(true, 0x3c) && __ec(false, 0x21c), 0x1c)), 0x0) }, sourceCode: [0x0: "(true && false)", 0x1c: "true && false", 0x3c: "true", 0x21c: "false"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(try x())"##: - ##"Testing.__checkValue(try x(), expression: .__fromSyntaxNode("try x()"), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"try Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try __ec(x(), 0x4) }, sourceCode: [0x4: "x()"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(1 is Int)"##: - ##"Testing.__checkCast(1, is: (Int).self, expression: .__fromBinaryOperation(.__fromSyntaxNode("1"), "is", .__fromSyntaxNode("Int")), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in __ec.__is(1, 0x0, (Int).self, 0x10) }, sourceCode: [0x0: "1 is Int", 0x10: "Int"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect("123") { 1 == 2 } then: { foo() }"##: - ##"Testing.__checkClosureCall(performing: { 1 == 2 }, then: { foo() }, expression: .__fromSyntaxNode("1 == 2"), comments: ["123"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkClosureCall(performing: { 1 == 2 }, then: { foo() }, sourceCode: [0x0: "1 == 2"], comments: ["123"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect("123") { let x = 0 }"##: - ##"Testing.__checkClosureCall(performing: { let x = 0 }, expression: .__fromSyntaxNode("let x = 0"), comments: ["123"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkClosureCall(performing: { let x = 0 }, sourceCode: [0x0: "let x = 0"], comments: ["123"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect("123") { let x = 0; return x == 0 }"##: - ##"Testing.__checkClosureCall(performing: { let x = 0; return x == 0 }, expression: .__fromSyntaxNode("{ let x = 0; return x == 0 }"), comments: ["123"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkClosureCall(performing: { let x = 0; return x == 0 }, sourceCode: [0x0: "{ let x = 0; return x == 0 }"], comments: ["123"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a, "b", c: c)"##: - ##"Testing.__checkValue(a, c: c, expression: .__fromSyntaxNode("a"), comments: ["b"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in __ec(a, 0x0) }, sourceCode: [0x0: "a"], c: c, comments: ["b"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a())"##: - ##"Testing.__checkFunctionCall((), calling: { _ in a() }, expression: .__fromFunctionCall(nil, "a"), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in __ec(a(), 0x0) }, sourceCode: [0x0: "a()"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(b(c))"##: - ##"Testing.__checkFunctionCall((), calling: { b($1) }, c, expression: .__fromFunctionCall(nil, "b", (nil, .__fromSyntaxNode("c"))), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in __ec(b(__ec(c, 0x70)), 0x0) }, sourceCode: [0x0: "b(c)", 0x70: "c"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a.b(c))"##: - ##"Testing.__checkFunctionCall(a.self, calling: { $0.b($1) }, c, expression: .__fromFunctionCall(.__fromSyntaxNode("a"), "b", (nil, .__fromSyntaxNode("c"))), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in __ec(__ec(a.self, 0x6).b(__ec(c, 0x700)), 0x0) }, sourceCode: [0x0: "a.b(c)", 0x6: "a", 0x700: "c"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a.b(c, d: e))"##: - ##"Testing.__checkFunctionCall(a.self, calling: { $0.b($1, d: $2) }, c, e, expression: .__fromFunctionCall(.__fromSyntaxNode("a"), "b", (nil, .__fromSyntaxNode("c")), ("d", .__fromSyntaxNode("e"))), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in __ec(__ec(a.self, 0x6).b(__ec(c, 0x700), d: __ec(e, 0x12100)), 0x0) }, sourceCode: [0x0: "a.b(c, d: e)", 0x6: "a", 0x700: "c", 0x12100: "e"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a.b(&c))"##: - ##"Testing.__checkInoutFunctionCall(a.self, calling: { $0.b(&$1) }, &c, expression: .__fromFunctionCall(.__fromSyntaxNode("a"), "b", (nil, .__fromSyntaxNode("&c"))), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, - ##"#expect(a.b(&c, &d))"##: - ##"Testing.__checkValue(a.b(&c, &d), expression: .__fromSyntaxNode("a.b(&c, &d)"), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in defer { __ec.__inoutAfter(c, 0x1700) } return __ec(__ec(a.self, 0x6).b(&c), 0x0) }, sourceCode: [0x0: "a.b(&c)", 0x6: "a", 0x1700: "c"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"#expect(a.b(&c, &d.e))"##: + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in defer { __ec.__inoutAfter(c, 0x1700) __ec.__inoutAfter(d.e, 0x58100) } return __ec(__ec(a.self, 0x6).b(&c, &d.e), 0x0) }, sourceCode: [0x0: "a.b(&c, &d.e)", 0x6: "a", 0x1700: "c", 0x58100: "d.e"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a.b(&c, d))"##: - ##"Testing.__checkValue(a.b(&c, d), expression: .__fromSyntaxNode("a.b(&c, d)"), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in defer { __ec.__inoutAfter(c, 0x1700) } return __ec(__ec(a.self, 0x6).b(&c, __ec(d, 0x18100)), 0x0) }, sourceCode: [0x0: "a.b(&c, d)", 0x6: "a", 0x1700: "c", 0x18100: "d"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a.b(try c()))"##: - ##"Testing.__checkValue(a.b(try c()), expression: .__fromSyntaxNode("a.b(try c())"), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"try Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(__ec(a.self, 0x6).b(try __ec(c(), 0x1700)), 0x0)) }, sourceCode: [0x0: "a.b(try c())", 0x6: "a", 0x1700: "c()"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a?.b(c))"##: - ##"Testing.__checkFunctionCall(a.self, calling: { $0?.b($1) }, c, expression: .__fromFunctionCall(.__fromSyntaxNode("a"), "b", (nil, .__fromSyntaxNode("c"))), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in __ec(__ec(a, 0xe)?.b(__ec(c, 0x1c00)), 0x0) }, sourceCode: [0x0: "a?.b(c)", 0xe: "a", 0x1c00: "c"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a???.b(c))"##: - ##"Testing.__checkFunctionCall(a.self, calling: { $0???.b($1) }, c, expression: .__fromFunctionCall(.__fromSyntaxNode("a"), "b", (nil, .__fromSyntaxNode("c"))), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in __ec(__ec(a, 0x3e)???.b(__ec(c, 0x1c000)), 0x0) }, sourceCode: [0x0: "a???.b(c)", 0x3e: "a", 0x1c000: "c"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a?.b.c(d))"##: - ##"Testing.__checkFunctionCall(a?.b.self, calling: { $0?.c($1) }, d, expression: .__fromFunctionCall(.__fromSyntaxNode("a?.b"), "c", (nil, .__fromSyntaxNode("d"))), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in __ec(__ec(a, 0x1e)?.b.c(__ec(d, 0x1c000)), 0x0) }, sourceCode: [0x0: "a?.b.c(d)", 0x1e: "a", 0x1c000: "d"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect({}())"##: - ##"Testing.__checkValue({}(), expression: .__fromSyntaxNode("{}()"), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in __ec({}(), 0x0) }, sourceCode: [0x0: "{}()"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a.b(c: d))"##: - ##"Testing.__checkFunctionCall(a.self, calling: { $0.b(c: $1) }, d, expression: .__fromFunctionCall(.__fromSyntaxNode("a"), "b", ("c", .__fromSyntaxNode("d"))), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in __ec(__ec(a.self, 0x6).b(c: __ec(d, 0x1300)), 0x0) }, sourceCode: [0x0: "a.b(c: d)", 0x6: "a", 0x1300: "d"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a.b { c })"##: - ##"Testing.__checkValue(a.b { c }, expression: .__fromSyntaxNode("a.b { c }"), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in __ec(__ec(a.self, 0x6).b { c }, 0x0) }, sourceCode: [0x0: "a.b { c }", 0x6: "a"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a, sourceLocation: someValue)"##: - ##"Testing.__checkValue(a, expression: .__fromSyntaxNode("a"), comments: [], isRequired: false, sourceLocation: someValue).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in __ec(a, 0x0) }, sourceCode: [0x0: "a"], comments: [], isRequired: false, sourceLocation: someValue).__expected()"##, ##"#expect(a.isB)"##: - ##"Testing.__checkPropertyAccess(a.self, getting: { $0.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a"), .__fromSyntaxNode("isB")), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in __ec(a.isB, 0x0) }, sourceCode: [0x0: "a.isB"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a???.isB)"##: - ##"Testing.__checkPropertyAccess(a.self, getting: { $0???.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a"), .__fromSyntaxNode("isB")), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in __ec(__ec(a, 0x1e)???.isB, 0x0) }, sourceCode: [0x0: "a???.isB", 0x1e: "a"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a?.b.isB)"##: - ##"Testing.__checkPropertyAccess(a?.b.self, getting: { $0?.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a?.b"), .__fromSyntaxNode("isB")), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in __ec(__ec(a, 0xe)?.b.isB, 0x0) }, sourceCode: [0x0: "a?.b.isB", 0xe: "a"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(a?.b().isB)"##: - ##"Testing.__checkPropertyAccess(a?.b().self, getting: { $0?.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a?.b()"), .__fromSyntaxNode("isB")), comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in __ec(__ec(__ec(a, 0x1e)?.b(), 0x2)?.isB, 0x0) }, sourceCode: [0x0: "a?.b().isB", 0x2: "a?.b()", 0x1e: "a"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(isolation: somewhere) {}"##: - ##"Testing.__checkClosureCall(performing: {}, expression: .__fromSyntaxNode("{}"), comments: [], isRequired: false, isolation: somewhere, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkClosureCall(performing: {}, sourceCode: [0x0: "{}"], comments: [], isRequired: false, isolation: somewhere, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ] ) func expectMacro(input: String, expectedOutput: String) throws { let (expectedOutput, _) = try parse(expectedOutput, removeWhitespace: true) let (actualOutput, _) = try parse(input, removeWhitespace: true) - #expect(expectedOutput == actualOutput) + let (actualActual, _) = try parse(input) + #expect(expectedOutput == actualOutput, "\(actualActual)") } @Test("#require() macro", arguments: [ ##"#require(true)"##: - ##"Testing.__checkValue(true, expression: .__fromSyntaxNode("true"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(true, 0x0)) }, sourceCode: [0x0: "true"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(false)"##: - ##"Testing.__checkValue(false, expression: .__fromSyntaxNode("false"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(false, 0x0)) }, sourceCode: [0x0: "false"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(false, "Custom message")"##: - ##"Testing.__checkValue(false, expression: .__fromSyntaxNode("false"), comments: ["Custom message"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(false, 0x0)) }, sourceCode: [0x0: "false"], comments: ["Custom message"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(2 > 1)"##: - ##"Testing.__checkBinaryOperation(2, { $0 > $1() }, 1, expression: .__fromBinaryOperation(.__fromSyntaxNode("2"), ">", .__fromSyntaxNode("1")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(2 > 1, 0x0)) }, sourceCode: [0x0: "2 > 1"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(((true || false) && true) || Bool.random())"##: - ##"Testing.__checkBinaryOperation(((true || false) && true), { $0 || $1() }, Bool.random(), expression: .__fromBinaryOperation(.__fromSyntaxNode("((true || false) && true)"), "||", .__fromSyntaxNode("Bool.random()")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(__ec((__ec(__ec((__ec(__ec(true, 0xf7a) || __ec(false, 0x877a), 0x77a)), 0x7a) && __ec(true, 0x10003a), 0x3a)), 0x2) || __ec(__ec(Bool.self, 0xe000000).random(), 0x2000000), 0x0)) }, sourceCode: [0x0: "((true || false) && true) || Bool.random()", 0x2: "((true || false) && true)", 0x3a: "(true || false) && true", 0x7a: "(true || false)", 0x77a: "true || false", 0xf7a: "true", 0x877a: "false", 0x10003a: "true", 0x2000000: "Bool.random()", 0xe000000: "Bool"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(9 > 8 && 7 > 6, "Some comment")"##: - ##"Testing.__checkBinaryOperation(9 > 8, { $0 && $1() }, 7 > 6, expression: .__fromBinaryOperation(.__fromSyntaxNode("9 > 8"), "&&", .__fromSyntaxNode("7 > 6")), comments: ["Some comment"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(__ec(9 > 8, 0x2) && __ec(7 > 6, 0x400), 0x0)) }, sourceCode: [0x0: "9 > 8 && 7 > 6", 0x2: "9 > 8", 0x400: "7 > 6"], comments: ["Some comment"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require("a" == "b")"##: - ##"Testing.__checkBinaryOperation("a", { $0 == $1() }, "b", expression: .__fromBinaryOperation(.__fromStringLiteral(#""a""#, "a"), "==", .__fromStringLiteral(#""b""#, "b")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec.__cmp({ $0 == $1 }, 0x0, "a", 0x2, "b", 0x200)) }, sourceCode: [0x0: #""a" == "b""#], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(!Bool.random())"##: - ##"Testing.__checkFunctionCall(Bool.self, calling: { $0.random() }, expression: .__fromNegation(.__fromFunctionCall(.__fromSyntaxNode("Bool"), "random"), false), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(!__ec(__ec(Bool.self, 0x1c).random(), 0x4), 0x0)) }, sourceCode: [0x0: "!Bool.random()", 0x4: "Bool.random()", 0x1c: "Bool"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require((true && false))"##: - ##"Testing.__checkBinaryOperation(true, { $0 && $1() }, false, expression: .__fromBinaryOperation(.__fromSyntaxNode("true"), "&&", .__fromSyntaxNode("false")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec((__ec(__ec(true, 0x3c) && __ec(false, 0x21c), 0x1c)), 0x0)) }, sourceCode: [0x0: "(true && false)", 0x1c: "true && false", 0x3c: "true", 0x21c: "false"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(try x())"##: - ##"Testing.__checkValue(try x(), expression: .__fromSyntaxNode("try x()"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try __ec(x(), 0x4) }, sourceCode: [0x4: "x()"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(1 is Int)"##: - ##"Testing.__checkCast(1, is: (Int).self, expression: .__fromBinaryOperation(.__fromSyntaxNode("1"), "is", .__fromSyntaxNode("Int")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec.__is(1, 0x0, (Int).self, 0x10)) }, sourceCode: [0x0: "1 is Int", 0x10: "Int"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require("123") { 1 == 2 } then: { foo() }"##: - ##"Testing.__checkClosureCall(performing: { 1 == 2 }, then: { foo() }, expression: .__fromSyntaxNode("1 == 2"), comments: ["123"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkClosureCall(performing: { 1 == 2 }, then: { foo() }, sourceCode: [0x0: "1 == 2"], comments: ["123"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require("123") { let x = 0 }"##: - ##"Testing.__checkClosureCall(performing: { let x = 0 }, expression: .__fromSyntaxNode("let x = 0"), comments: ["123"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkClosureCall(performing: { let x = 0 }, sourceCode: [0x0: "let x = 0"], comments: ["123"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require("123") { let x = 0; return x == 0 }"##: - ##"Testing.__checkClosureCall(performing: { let x = 0; return x == 0 }, expression: .__fromSyntaxNode("{ let x = 0; return x == 0 }"), comments: ["123"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkClosureCall(performing: { let x = 0; return x == 0 }, sourceCode: [0x0: "{ let x = 0; return x == 0 }"], comments: ["123"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a, "b", c: c)"##: - ##"Testing.__checkValue(a, c: c, expression: .__fromSyntaxNode("a"), comments: ["b"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(a, 0x0)) }, sourceCode: [0x0: "a"], c: c, comments: ["b"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a())"##: - ##"Testing.__checkFunctionCall((), calling: { _ in a() }, expression: .__fromFunctionCall(nil, "a"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(a(), 0x0)) }, sourceCode: [0x0: "a()"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(b(c))"##: - ##"Testing.__checkFunctionCall((), calling: { b($1) }, c, expression: .__fromFunctionCall(nil, "b", (nil, .__fromSyntaxNode("c"))), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(b(__ec(c, 0x70)), 0x0)) }, sourceCode: [0x0: "b(c)", 0x70: "c"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a.b(c))"##: - ##"Testing.__checkFunctionCall(a.self, calling: { $0.b($1) }, c, expression: .__fromFunctionCall(.__fromSyntaxNode("a"), "b", (nil, .__fromSyntaxNode("c"))), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(__ec(a.self, 0x6).b(__ec(c, 0x700)), 0x0)) }, sourceCode: [0x0: "a.b(c)", 0x6: "a", 0x700: "c"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a.b(c, d: e))"##: - ##"Testing.__checkFunctionCall(a.self, calling: { $0.b($1, d: $2) }, c, e, expression: .__fromFunctionCall(.__fromSyntaxNode("a"), "b", (nil, .__fromSyntaxNode("c")), ("d", .__fromSyntaxNode("e"))), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(__ec(a.self, 0x6).b(__ec(c, 0x700), d: __ec(e, 0x12100)), 0x0)) }, sourceCode: [0x0: "a.b(c, d: e)", 0x6: "a", 0x700: "c", 0x12100: "e"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a.b(&c))"##: - ##"Testing.__checkInoutFunctionCall(a.self, calling: { $0.b(&$1) }, &c, expression: .__fromFunctionCall(.__fromSyntaxNode("a"), "b", (nil, .__fromSyntaxNode("&c"))), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, - ##"#require(a.b(&c, &d))"##: - ##"Testing.__checkValue(a.b(&c, &d), expression: .__fromSyntaxNode("a.b(&c, &d)"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in defer { __ec.__inoutAfter(c, 0x1700) } return try Testing.__requiringTry(__ec(__ec(a.self, 0x6).b(&c), 0x0)) }, sourceCode: [0x0: "a.b(&c)", 0x6: "a", 0x1700: "c"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"#require(a.b(&c, &d.e))"##: + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in defer { __ec.__inoutAfter(c, 0x1700) __ec.__inoutAfter(d.e, 0x58100) } return try Testing.__requiringTry(__ec(__ec(a.self, 0x6).b(&c, &d.e), 0x0)) }, sourceCode: [0x0: "a.b(&c, &d.e)", 0x6: "a", 0x1700: "c", 0x58100: "d.e"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a.b(&c, d))"##: - ##"Testing.__checkValue(a.b(&c, d), expression: .__fromSyntaxNode("a.b(&c, d)"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in defer { __ec.__inoutAfter(c, 0x1700) } return try Testing.__requiringTry(__ec(__ec(a.self, 0x6).b(&c, __ec(d, 0x18100)), 0x0)) }, sourceCode: [0x0: "a.b(&c, d)", 0x6: "a", 0x1700: "c", 0x18100: "d"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a.b(try c()))"##: - ##"Testing.__checkValue(a.b(try c()), expression: .__fromSyntaxNode("a.b(try c())"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(__ec(a.self, 0x6).b(try __ec(c(), 0x1700)), 0x0)) }, sourceCode: [0x0: "a.b(try c())", 0x6: "a", 0x1700: "c()"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a?.b(c))"##: - ##"Testing.__checkFunctionCall(a.self, calling: { $0?.b($1) }, c, expression: .__fromFunctionCall(.__fromSyntaxNode("a"), "b", (nil, .__fromSyntaxNode("c"))), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(__ec(a, 0xe)?.b(__ec(c, 0x1c00)), 0x0)) }, sourceCode: [0x0: "a?.b(c)", 0xe: "a", 0x1c00: "c"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a???.b(c))"##: - ##"Testing.__checkFunctionCall(a.self, calling: { $0???.b($1) }, c, expression: .__fromFunctionCall(.__fromSyntaxNode("a"), "b", (nil, .__fromSyntaxNode("c"))), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(__ec(a, 0x3e)???.b(__ec(c, 0x1c000)), 0x0)) }, sourceCode: [0x0: "a???.b(c)", 0x3e: "a", 0x1c000: "c"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a?.b.c(d))"##: - ##"Testing.__checkFunctionCall(a?.b.self, calling: { $0?.c($1) }, d, expression: .__fromFunctionCall(.__fromSyntaxNode("a?.b"), "c", (nil, .__fromSyntaxNode("d"))), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(__ec(a, 0x1e)?.b.c(__ec(d, 0x1c000)), 0x0)) }, sourceCode: [0x0: "a?.b.c(d)", 0x1e: "a", 0x1c000: "d"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require({}())"##: - ##"Testing.__checkValue({}(), expression: .__fromSyntaxNode("{}()"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec({}(), 0x0)) }, sourceCode: [0x0: "{}()"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a.b(c: d))"##: - ##"Testing.__checkFunctionCall(a.self, calling: { $0.b(c: $1) }, d, expression: .__fromFunctionCall(.__fromSyntaxNode("a"), "b", ("c", .__fromSyntaxNode("d"))), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(__ec(a.self, 0x6).b(c: __ec(d, 0x1300)), 0x0)) }, sourceCode: [0x0: "a.b(c: d)", 0x6: "a", 0x1300: "d"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a.b { c })"##: - ##"Testing.__checkValue(a.b { c }, expression: .__fromSyntaxNode("a.b { c }"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(__ec(a.self, 0x6).b { c }, 0x0)) }, sourceCode: [0x0: "a.b { c }", 0x6: "a"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a, sourceLocation: someValue)"##: - ##"Testing.__checkValue(a, expression: .__fromSyntaxNode("a"), comments: [], isRequired: true, sourceLocation: someValue).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(a, 0x0)) }, sourceCode: [0x0: "a"], comments: [], isRequired: true, sourceLocation: someValue).__required()"##, ##"#require(a.isB)"##: - ##"Testing.__checkPropertyAccess(a.self, getting: { $0.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a"), .__fromSyntaxNode("isB")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(a.isB, 0x0)) }, sourceCode: [0x0: "a.isB"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a???.isB)"##: - ##"Testing.__checkPropertyAccess(a.self, getting: { $0???.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a"), .__fromSyntaxNode("isB")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(__ec(a, 0x1e)???.isB, 0x0)) }, sourceCode: [0x0: "a???.isB", 0x1e: "a"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a?.b.isB)"##: - ##"Testing.__checkPropertyAccess(a?.b.self, getting: { $0?.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a?.b"), .__fromSyntaxNode("isB")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(__ec(a, 0xe)?.b.isB, 0x0)) }, sourceCode: [0x0: "a?.b.isB", 0xe: "a"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(a?.b().isB)"##: - ##"Testing.__checkPropertyAccess(a?.b().self, getting: { $0?.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a?.b()"), .__fromSyntaxNode("isB")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(__ec(__ec(a, 0x1e)?.b(), 0x2)?.isB, 0x0)) }, sourceCode: [0x0: "a?.b().isB", 0x2: "a?.b()", 0x1e: "a"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(isolation: somewhere) {}"##: - ##"Testing.__checkClosureCall(performing: {}, expression: .__fromSyntaxNode("{}"), comments: [], isRequired: true, isolation: somewhere, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkClosureCall(performing: {}, sourceCode: [0x0: "{}"], comments: [], isRequired: true, isolation: somewhere, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ] ) func requireMacro(input: String, expectedOutput: String) throws { let (expectedOutput, _) = try parse(expectedOutput, removeWhitespace: true) let (actualOutput, _) = try parse(input, removeWhitespace: true) - #expect(expectedOutput == actualOutput) + let (actualActual, _) = try parse(input) + #expect(expectedOutput == actualOutput, "\(actualActual)") } @Test("Unwrapping #require() macro", arguments: [ - ##"#require(Optional.none)"##: - ##"Testing.__checkPropertyAccess(Optional.self, getting: { $0.none }, expression: .__fromPropertyAccess(.__fromSyntaxNode("Optional"), .__fromSyntaxNode("none")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, - ##"#require(nil ?? 123)"##: - ##"Testing.__checkBinaryOperation(nil, { $0 ?? $1() }, 123, expression: .__fromBinaryOperation(.__fromSyntaxNode("nil"), "??", .__fromSyntaxNode("123")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, - ##"#require(123 ?? nil)"##: - ##"Testing.__checkBinaryOperation(123, { $0 ?? $1() }, nil, expression: .__fromBinaryOperation(.__fromSyntaxNode("123"), "??", .__fromSyntaxNode("nil")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, - ##"#require(123 as? Double)"##: - ##"Testing.__checkCast(123,as: (Double).self, expression: .__fromBinaryOperation(.__fromSyntaxNode("123"), "as?", .__fromSyntaxNode("Double")), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, - ##"#require(123 as Double)"##: - ##"Testing.__checkValue(123 as Double, expression: .__fromSyntaxNode("123 as Double"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, - ##"#require(123 as! Double)"##: - ##"Testing.__checkValue(123 as! Double, expression: .__fromSyntaxNode("123 as! Double"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"#requireUnwrap(Optional.none)"##: + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Optional in try Testing.__requiringTry(__ec(Optional.none, 0x0)) }, sourceCode: [0x0: "Optional.none"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"#requireUnwrap(nil ?? 123)"##: + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Optional in try Testing.__requiringTry(__ec(nil ?? 123, 0x0)) }, sourceCode: [0x0: "nil ?? 123"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"#requireUnwrap(123 ?? nil)"##: + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Optional in try Testing.__requiringTry(__ec(123 ?? nil, 0x0)) }, sourceCode: [0x0: "123 ?? nil"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"#requireUnwrap(123 as? Double)"##: + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Optional in try Testing.__requiringTry(__ec.__as(123, 0x0, (Double).self, 0x20)) }, sourceCode: [0x0: "123 as? Double", 0x20: "Double"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"#requireUnwrap(123 as Double)"##: + ##"Testing.__checkEscapedCondition(123 as Double, sourceCode: [0x0: "123 as Double"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"#requireUnwrap(123 as! Double)"##: + ##"Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Optional in try Testing.__requiringTry(__ec(123 as! Double, 0x0)) }, sourceCode: [0x0: "123 as! Double"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ] ) func unwrappingRequireMacro(input: String, expectedOutput: String) throws { - let (expectedOutput, _) = try parse(expectedOutput) - let (actualOutput, _) = try parse(input) - #expect(expectedOutput == actualOutput) + let (expectedOutput, _) = try parse(expectedOutput, removeWhitespace: true) + let (actualOutput, _) = try parse(input, removeWhitespace: true) + let (actualActual, _) = try parse(input) + #expect(expectedOutput == actualOutput, "\(actualActual)") + } + + @Test("Deep expression IDs", arguments: [ + ##"#expect(a(b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q)"##: + ##"__ec(q, Testing.__ExpressionID(66, 65, 4))"##, + ]) func deepExpressionID(input: String, expectedOutput: String) throws { + let (expectedOutput, _) = try parse(expectedOutput, removeWhitespace: true) + let (actualOutput, _) = try parse(input, removeWhitespace: true) + let (actualActual, _) = try parse(input) + #expect(actualOutput.contains(expectedOutput), "\(actualActual)") } @Test("Capturing comments above #expect()/#require()", @@ -212,7 +225,7 @@ struct ConditionMacroTests { """ // Source comment /** Doc comment */ - Testing.__checkValue(try x(), expression: .__fromSyntaxNode("try x()"), comments: [.__line("// Source comment"),.__documentationBlock("/** Doc comment */"),"Argument comment"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected() + try Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try __ec(x(), 0x4) }, sourceCode: [0x4: "x()"], comments: [.__line("// Source comment"),.__documentationBlock("/** Doc comment */"),"Argument comment"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected() """, """ @@ -225,7 +238,7 @@ struct ConditionMacroTests { // Ignore me // Capture me - Testing.__checkValue(try x(), expression: .__fromSyntaxNode("try x()"), comments: [.__line("// Capture me")], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected() + try Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try __ec(x(), 0x4) }, sourceCode: [0x4: "x()"], comments: [.__line("// Capture me")], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected() """, """ @@ -238,14 +251,15 @@ struct ConditionMacroTests { // Ignore me \t // Capture me - Testing.__checkValue(try x(), expression: .__fromSyntaxNode("try x()"), comments: [.__line("// Capture me")], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected() + try Testing.__checkCondition({ (__ec: inout Testing.__ExpectationContext) -> Swift.Bool in try __ec(x(), 0x4) }, sourceCode: [0x4: "x()"], comments: [.__line("// Capture me")], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected() """, ] ) func commentCapture(input: String, expectedOutput: String) throws { - let (expectedOutput, _) = try parse(expectedOutput) - let (actualOutput, _) = try parse(input) - #expect(expectedOutput == actualOutput) + let (expectedOutput, _) = try parse(expectedOutput, removeWhitespace: true) + let (actualOutput, _) = try parse(input, removeWhitespace: true) + let (actualActual, _) = try parse(input) + #expect(expectedOutput == actualOutput, "\(actualActual)") } @Test("#expect(false) and #require(false) warn they always fail", @@ -366,25 +380,4 @@ struct ConditionMacroTests { #expect(diagnostic.diagMessage.severity == .warning) #expect(diagnostic.message.contains("is redundant")) } - - @Test("Macro expansion is performed within a test function") - func macroExpansionInTestFunction() throws { - let input = ##""" - @Test("Random number generation") func rng() { - let number = Int.random(in: 1 ..< .max) - #expect((number > 0 && foo() != bar(at: 9)) != !true, "\(number) must be greater than 0") - } - """## - - let rawExpectedOutput = ##""" - @Test("Random number generation") func rng() { - let number = Int.random(in: 1 ..< .max) - Testing.__checkBinaryOperation((number > 0 && foo() != bar(at: 9)), { $0 != $1() }, !true, expression: .__fromBinaryOperation(.__fromSyntaxNode("(number > 0 && foo() != bar(at: 9))"), "!=", .__fromSyntaxNode("!true")), comments: ["\(number) must be greater than 0"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected() - } - """## - - let (expectedOutput, _) = try parse(rawExpectedOutput, activeMacros: ["expect"], removeWhitespace: true) - let (actualOutput, _) = try parse(input, activeMacros: ["expect"], removeWhitespace: true) - #expect(expectedOutput == actualOutput) - } } diff --git a/Tests/TestingMacrosTests/TestSupport/Parse.swift b/Tests/TestingMacrosTests/TestSupport/Parse.swift index ecff8de58..c2ff1f933 100644 --- a/Tests/TestingMacrosTests/TestSupport/Parse.swift +++ b/Tests/TestingMacrosTests/TestSupport/Parse.swift @@ -21,6 +21,7 @@ import SwiftSyntaxMacroExpansion fileprivate let allMacros: [String: any Macro.Type] = [ "expect": ExpectMacro.self, "require": RequireMacro.self, + "requireUnwrap": UnwrapMacro.self, // different name needed only for unit testing "requireAmbiguous": AmbiguousRequireMacro.self, // different name needed only for unit testing "requireNonOptional": NonOptionalRequireMacro.self, // different name needed only for unit testing "requireThrows": RequireThrowsMacro.self, // different name needed only for unit testing diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 98ecc668d..b48ddc572 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -22,6 +22,12 @@ import CoreGraphics import UniformTypeIdentifiers #endif +extension Character { + var isPathSeparator: Bool { + self == "/" || self == #"\"# + } +} + @Suite("Attachment Tests") struct AttachmentTests { @Test func saveValue() { @@ -91,7 +97,7 @@ struct AttachmentTests { // Write the attachment to disk, then read it back. let filePath = try attachment.write(toFileInDirectoryAtPath: temporaryDirectory(), appending: suffixes.next()!) createdFilePaths.append(filePath) - let fileName = try #require(filePath.split { $0 == "/" || $0 == #"\"# }.last) + let fileName = try #require(filePath.split(whereSeparator: \.isPathSeparator).last) if i == 0 { #expect(fileName == baseFileName) } else { @@ -118,7 +124,7 @@ struct AttachmentTests { defer { remove(filePath) } - let fileName = try #require(filePath.split { $0 == "/" || $0 == #"\"# }.last) + let fileName = try #require(filePath.split(whereSeparator: \.isPathSeparator).last) #expect(fileName == "loremipsum-\(suffix).tgz.gif.jpeg.html") try compare(attachableValue, toContentsOfFileAtPath: filePath) } @@ -584,7 +590,7 @@ struct MySendableAttachable: Attachable, Sendable { var string: String func withUnsafeBufferPointer(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - #expect(attachment.attachableValue.string == string) + #expect(attachment.attachableValue.string == self.string) var string = string return try string.withUTF8 { buffer in try body(.init(buffer)) diff --git a/Tests/TestingTests/EventRecorderTests.swift b/Tests/TestingTests/EventRecorderTests.swift index 97619b755..d762e8e41 100644 --- a/Tests/TestingTests/EventRecorderTests.swift +++ b/Tests/TestingTests/EventRecorderTests.swift @@ -108,8 +108,8 @@ struct EventRecorderTests { await runTest(for: WrittenTests.self, configuration: configuration) let buffer = stream.buffer.rawValue - #expect(buffer.contains(#"\#(Event.Symbol.details.unicodeCharacter) "abc": Swift.String"#)) - #expect(buffer.contains(#"\#(Event.Symbol.details.unicodeCharacter) lhs: Swift.String → "987""#)) + #expect(buffer.contains(#"\#(Event.Symbol.details.unicodeCharacter) "abc" == "xyz": Swift.Bool → false"#)) + #expect(buffer.contains(#"\#(Event.Symbol.details.unicodeCharacter) lhs: Swift.String → "987""#)) #expect(buffer.contains(#""Animal Crackers" (aka 'WrittenTests')"#)) #expect(buffer.contains(#""Not A Lobster" (aka 'actuallyCrab()')"#)) diff --git a/Tests/TestingTests/EventTests.swift b/Tests/TestingTests/EventTests.swift index 941dcadb9..14eca5b84 100644 --- a/Tests/TestingTests/EventTests.swift +++ b/Tests/TestingTests/EventTests.swift @@ -21,7 +21,6 @@ struct EventTests { Expectation( evaluatedExpression: __Expression("SyntaxNode"), mismatchedErrorDescription: "Mismatched Error Description", - differenceDescription: "Difference Description", isPassing: false, isRequired: true, sourceLocation: SourceLocation(fileID: "M/f.swift", filePath: "/f.swift", line: 1, column: 1) diff --git a/Tests/TestingTests/IssueTests.swift b/Tests/TestingTests/IssueTests.swift index 4a4fda631..159a5c941 100644 --- a/Tests/TestingTests/IssueTests.swift +++ b/Tests/TestingTests/IssueTests.swift @@ -14,6 +14,22 @@ private import _TestingInternals #if canImport(XCTest) import XCTest +func expression(_ expression: __Expression, contains string: String) -> Bool { + if expression.expandedDescription().contains(string) { + return true + } + + return expression.subexpressions.contains { TestingTests.expression($0, contains: string) } +} + +func assert(_ expression: __Expression, contains string: String) { + XCTAssertTrue(TestingTests.expression(expression, contains: string), "\(expression) did not contain \(string)") +} + +func assert(_ expression: __Expression, doesNotContain string: String) { + XCTAssertFalse(TestingTests.expression(expression, contains: string), "\(expression) did not contain \(string)") +} + final class IssueTests: XCTestCase { func testExpect() async throws { var configuration = Configuration() @@ -55,7 +71,7 @@ final class IssueTests: XCTestCase { }.run(configuration: configuration) await Test { () throws in - try #expect({ throw MyError() }()) + #expect(try { throw MyError() }()) }.run(configuration: configuration) } @@ -160,9 +176,8 @@ final class IssueTests: XCTestCase { } if case let .expectationFailed(expectation) = issue.kind { expectationFailed.fulfill() - let desc = expectation.evaluatedExpression.expandedDescription() - XCTAssertTrue(desc.contains("rhs → 1")) - XCTAssertFalse(desc.contains("((")) + assert(expectation.evaluatedExpression, contains: "TypeWithMemberFunctions.f(rhs) → false") + assert(expectation.evaluatedExpression, contains: "rhs → 1") } } @@ -184,9 +199,8 @@ final class IssueTests: XCTestCase { } if case let .expectationFailed(expectation) = issue.kind { expectationFailed.fulfill() - let desc = expectation.evaluatedExpression.expandedDescription() - XCTAssertTrue(desc.contains("label: rhs → 1")) - XCTAssertFalse(desc.contains("((")) + assert(expectation.evaluatedExpression, contains: "TypeWithMemberFunctions.g(label: rhs) → false") + assert(expectation.evaluatedExpression, contains: "rhs → 1") } } @@ -208,9 +222,8 @@ final class IssueTests: XCTestCase { } if case let .expectationFailed(expectation) = issue.kind { expectationFailed.fulfill() - let desc = expectation.evaluatedExpression.expandedDescription() - XCTAssertFalse(desc.contains("(Function)")) - XCTAssertFalse(desc.contains("((")) + assert(expectation.evaluatedExpression, contains: "TypeWithMemberFunctions.h({ }) → false") + assert(expectation.evaluatedExpression, doesNotContain: "(Function)") } } @@ -276,9 +289,8 @@ final class IssueTests: XCTestCase { } if case let .expectationFailed(expectation) = issue.kind { expectationFailed.fulfill() - // The presence of `try` means we don't do complex expansion (yet.) XCTAssertNotNil(expectation.evaluatedExpression) - XCTAssertNil(expectation.evaluatedExpression.runtimeValue) + XCTAssertNotNil(expectation.evaluatedExpression.runtimeValue) } } @@ -318,7 +330,7 @@ final class IssueTests: XCTestCase { } func testExpressionLiterals() async { - func expectIssue(containing content: String, in testFunction: @escaping @Sendable () async throws -> Void) async { + func expectIssue(containing content: String..., in testFunction: @escaping @Sendable () async throws -> Void) async { let issueRecorded = expectation(description: "Issue recorded") var configuration = Configuration() @@ -328,8 +340,9 @@ final class IssueTests: XCTestCase { return } XCTAssertTrue(issue.comments.isEmpty) - let expandedExpressionDescription = expectation.evaluatedExpression.expandedDescription() - XCTAssert(expandedExpressionDescription.contains(content)) + for content in content { + assert(expectation.evaluatedExpression, contains: content) + } issueRecorded.fulfill() } @@ -340,13 +353,13 @@ final class IssueTests: XCTestCase { @Sendable func someInt() -> Int { 0 } @Sendable func someString() -> String { "a" } - await expectIssue(containing: "(someInt() → 0) == 1") { + await expectIssue(containing: "someInt() == 1 → false", "someInt() → 0") { #expect(someInt() == 1) } - await expectIssue(containing: "1 == (someInt() → 0)") { + await expectIssue(containing: "1 == someInt() → false", "someInt() → 0") { #expect(1 == someInt()) } - await expectIssue(containing: "(someString() → \"a\") == \"b\"") { + await expectIssue(containing: #"someString() == "b" → false"#, #"someString() → "a""#) { #expect(someString() == "b") } } @@ -354,12 +367,12 @@ final class IssueTests: XCTestCase { struct ExpressionRuntimeValueCapture_Value {} func testExpressionRuntimeValueCapture() throws { - var expression = __Expression.__fromSyntaxNode("abc123") + var expression = __Expression("abc123") XCTAssertEqual(expression.sourceCode, "abc123") XCTAssertNil(expression.runtimeValue) do { - expression = expression.capturingRuntimeValues(987 as Int) + expression.runtimeValue = __Expression.Value(reflecting: 987 as Int) XCTAssertEqual(expression.sourceCode, "abc123") let runtimeValue = try XCTUnwrap(expression.runtimeValue) XCTAssertEqual(String(describing: runtimeValue), "987") @@ -368,7 +381,7 @@ final class IssueTests: XCTestCase { } do { - expression = expression.capturingRuntimeValues(ExpressionRuntimeValueCapture_Value()) + expression.runtimeValue = __Expression.Value(reflecting: ExpressionRuntimeValueCapture_Value()) XCTAssertEqual(expression.sourceCode, "abc123") let runtimeValue = try XCTUnwrap(expression.runtimeValue) XCTAssertEqual(String(describing: runtimeValue), "ExpressionRuntimeValueCapture_Value()") @@ -377,7 +390,7 @@ final class IssueTests: XCTestCase { } do { - expression = expression.capturingRuntimeValues((123, "abc") as (Int, String), ()) + expression.runtimeValue = __Expression.Value(reflecting: (123, "abc") as (Int, String)) XCTAssertEqual(expression.sourceCode, "abc123") let runtimeValue = try XCTUnwrap(expression.runtimeValue) XCTAssertEqual(String(describing: runtimeValue), #"(123, "abc")"#) @@ -391,12 +404,12 @@ final class IssueTests: XCTestCase { } func testExpressionRuntimeValueChildren() throws { - var expression = __Expression.__fromSyntaxNode("abc123") + var expression = __Expression("abc123") XCTAssertEqual(expression.sourceCode, "abc123") XCTAssertNil(expression.runtimeValue) do { - expression = expression.capturingRuntimeValues(ExpressionRuntimeValueCapture_Value()) + expression.runtimeValue = __Expression.Value(reflecting: ExpressionRuntimeValueCapture_Value()) let runtimeValue = try XCTUnwrap(expression.runtimeValue) XCTAssertEqual(String(describing: runtimeValue), "ExpressionRuntimeValueCapture_Value()") XCTAssertEqual(runtimeValue.typeInfo.fullyQualifiedName, "TestingTests.IssueTests.ExpressionRuntimeValueCapture_Value") @@ -406,7 +419,7 @@ final class IssueTests: XCTestCase { } do { - expression = expression.capturingRuntimeValues(ExpressionRuntimeValueCapture_ValueWithChildren(contents: [123, "abc"])) + expression.runtimeValue = __Expression.Value(reflecting: ExpressionRuntimeValueCapture_ValueWithChildren(contents: [123, "abc"])) let runtimeValue = try XCTUnwrap(expression.runtimeValue) XCTAssertEqual(String(describing: runtimeValue), #"ExpressionRuntimeValueCapture_ValueWithChildren(contents: [123, "abc"])"#) XCTAssertEqual(runtimeValue.typeInfo.fullyQualifiedName, "TestingTests.IssueTests.ExpressionRuntimeValueCapture_ValueWithChildren") @@ -429,7 +442,7 @@ final class IssueTests: XCTestCase { } do { - expression = expression.capturingRuntimeValues([]) + expression.runtimeValue = __Expression.Value(reflecting: []) let runtimeValue = try XCTUnwrap(expression.runtimeValue) XCTAssertEqual(String(describing: runtimeValue), "[]") XCTAssertEqual(runtimeValue.typeInfo.fullyQualifiedName, "Swift.Array") @@ -455,9 +468,8 @@ final class IssueTests: XCTestCase { XCTFail("Unexpected issue kind \(issue.kind)") return } - let expandedExpressionDescription = expectation.evaluatedExpression.expandedDescription() - XCTAssertTrue(expandedExpressionDescription.contains("someString() → \"abc123\"")) - XCTAssertTrue(expandedExpressionDescription.contains("Int → String")) + assert(expectation.evaluatedExpression, contains: #"someString() → "abc123""#) + assert(expectation.evaluatedExpression, contains: "Int → String") if expectation.isRequired { requireRecorded.fulfill() @@ -1133,15 +1145,14 @@ final class IssueTests: XCTestCase { } } - func testCollectionDifference() async { + func testCollectionDifference() async throws { var configuration = Configuration() configuration.eventHandler = { event, _ in guard case let .issueRecorded(issue) = event.kind else { return } guard case let .expectationFailed(expectation) = issue.kind else { - XCTFail("Unexpected issue kind \(issue.kind)") - return + return XCTFail("Unexpected issue kind \(issue.kind)") } guard let differenceDescription = expectation.differenceDescription else { return XCTFail("Unexpected nil differenceDescription") @@ -1157,7 +1168,28 @@ final class IssueTests: XCTestCase { }.run(configuration: configuration) } - func testCollectionDifferenceSkippedForStrings() async { + func testCollectionDifferenceForStrings() async throws { + var configuration = Configuration() + configuration.eventHandler = { event, _ in + guard case let .issueRecorded(issue) = event.kind else { + return + } + guard case let .expectationFailed(expectation) = issue.kind else { + return XCTFail("Unexpected issue kind \(issue.kind)") + } + guard let differenceDescription = expectation.differenceDescription else { + return XCTFail("Unexpected nil differenceDescription") + } + XCTAssertTrue(differenceDescription.contains(#"inserted ["hello""#)) + XCTAssertTrue(differenceDescription.contains(#"removed ["helbo""#)) + } + + await Test { + #expect("hello\nworld" == "helbo\nworld") + }.run(configuration: configuration) + } + + func testCollectionDifferenceSkippedForStringsWithoutNewlines() async throws { var configuration = Configuration() configuration.eventHandler = { event, _ in guard case let .issueRecorded(issue) = event.kind else { @@ -1175,7 +1207,25 @@ final class IssueTests: XCTestCase { }.run(configuration: configuration) } - func testCollectionDifferenceSkippedForRanges() async { + func testCollectionDifferenceSkippedForStringsWithCharacterDifferencesOnly() async throws { + var configuration = Configuration() + configuration.eventHandler = { event, _ in + guard case let .issueRecorded(issue) = event.kind else { + return + } + guard case let .expectationFailed(expectation) = issue.kind else { + XCTFail("Unexpected issue kind \(issue.kind)") + return + } + XCTAssertNil(expectation.differenceDescription) + } + + await Test { + #expect("hello\n" == "hello\r") + }.run(configuration: configuration) + } + + func testCollectionDifferenceSkippedForRanges() async throws { var configuration = Configuration() configuration.eventHandler = { event, _ in guard case let .issueRecorded(issue) = event.kind else { @@ -1238,33 +1288,6 @@ final class IssueTests: XCTestCase { }.run(configuration: configuration) } - func testNegatedExpressionsExpandToCaptureNegatedExpression() async { - var configuration = Configuration() - configuration.eventHandler = { event, _ in - guard case let .issueRecorded(issue) = event.kind else { - return - } - guard case let .expectationFailed(expectation) = issue.kind else { - XCTFail("Unexpected issue \(issue)") - return - } - XCTAssertNotNil(expectation.evaluatedExpression.runtimeValue) - XCTAssertTrue(expectation.evaluatedExpression.runtimeValue!.typeInfo.describes(Bool.self)) - guard case let .negation(subexpression, isParenthetical) = expectation.evaluatedExpression.kind else { - XCTFail("Expected expression's kind was negation, but it was \(expectation.evaluatedExpression.kind)") - return - } - XCTAssertTrue(isParenthetical) - XCTAssertNotNil(subexpression.runtimeValue) - XCTAssertTrue(subexpression.runtimeValue!.typeInfo.describes(Bool.self)) - } - - @Sendable func g() -> Int { 1 } - await Test { - #expect(!(g() == 1)) - }.run(configuration: configuration) - } - func testLazyExpectDoesNotEvaluateRightHandValue() async { var configuration = Configuration() configuration.eventHandler = { event, _ in @@ -1277,7 +1300,7 @@ final class IssueTests: XCTestCase { return } let expression = expectation.evaluatedExpression - XCTAssertTrue(expression.expandedDescription().contains("")) + assert(expression, contains: "") } @Sendable func rhs() -> Bool { @@ -1336,9 +1359,8 @@ final class IssueTests: XCTestCase { } if case let .expectationFailed(expectation) = issue.kind { expectationFailed.fulfill() - let desc = expectation.evaluatedExpression.expandedDescription() - XCTAssertTrue(desc.contains("7")) - XCTAssertFalse(desc.contains("Optional(7)")) + assert(expectation.evaluatedExpression, contains: "7") + assert(expectation.evaluatedExpression, doesNotContain: "Optional(7)") } } @@ -1360,8 +1382,7 @@ final class IssueTests: XCTestCase { } if case let .expectationFailed(expectation) = issue.kind { expectationFailed.fulfill() - let desc = expectation.evaluatedExpression.expandedDescription() - XCTAssertTrue(desc.contains("nil")) + assert(expectation.evaluatedExpression, contains: "nil") } } @@ -1414,8 +1435,7 @@ final class IssueTests: XCTestCase { } if case let .expectationFailed(expectation) = issue.kind { expectationFailed.fulfill() - let desc = expectation.evaluatedExpression.expandedDescription() - XCTAssertTrue(desc.contains("Delicious Food, Yay!")) + assert(expectation.evaluatedExpression, contains: "Delicious Food, Yay!") } } @@ -1476,9 +1496,8 @@ final class IssueTests: XCTestCase { } if case let .expectationFailed(expectation) = issue.kind { expectationFailed.fulfill() - let desc = expectation.evaluatedExpression.expandedDescription() - XCTAssertTrue(desc.contains(".b → customDesc")) - XCTAssertFalse(desc.contains(".customDesc")) + assert(expectation.evaluatedExpression, contains: ".b → customDesc") + assert(expectation.evaluatedExpression, doesNotContain: ".customDesc") } } @@ -1503,9 +1522,8 @@ final class IssueTests: XCTestCase { } if case let .expectationFailed(expectation) = issue.kind { expectationFailed.fulfill() - let desc = expectation.evaluatedExpression.expandedDescription() - XCTAssertTrue(desc.contains(".A → SWTTestEnumeration(rawValue: \(SWTTestEnumeration.A.rawValue))")) - XCTAssertFalse(desc.contains(".SWTTestEnumeration")) + assert(expectation.evaluatedExpression, contains: ".A → SWTTestEnumeration(rawValue: \(SWTTestEnumeration.A.rawValue))") + assert(expectation.evaluatedExpression, doesNotContain: ".SWTTestEnumeration") } } @@ -1527,7 +1545,7 @@ struct IssueCodingTests { private static let issueKinds: [Issue.Kind] = [ Issue.Kind.apiMisused, Issue.Kind.errorCaught(NSError(domain: "Domain", code: 13, userInfo: ["UserInfoKey": "UserInfoValue"])), - Issue.Kind.expectationFailed(Expectation(evaluatedExpression: .__fromSyntaxNode("abc"), isPassing: true, isRequired: true, sourceLocation: #_sourceLocation)), + Issue.Kind.expectationFailed(Expectation(evaluatedExpression: .init("abc"), isPassing: true, isRequired: true, sourceLocation: #_sourceLocation)), Issue.Kind.knownIssueNotRecorded, Issue.Kind.system, Issue.Kind.timeLimitExceeded(timeLimitComponents: (13, 42)), diff --git a/Tests/TestingTests/MiscellaneousTests.swift b/Tests/TestingTests/MiscellaneousTests.swift index a172f7d5a..5685494d9 100644 --- a/Tests/TestingTests/MiscellaneousTests.swift +++ b/Tests/TestingTests/MiscellaneousTests.swift @@ -262,6 +262,11 @@ struct MultiLineSuite { _ = try #require(x?[...].last) } +@Test(.hidden) func canHaveVariableNamed__ec() throws { + let __ec = 1 + #expect(__ec == 1) +} + @Suite("Miscellaneous tests") struct MiscellaneousTests { @Test("Free function's name") diff --git a/Tests/TestingTests/Support/CartesianProductTests.swift b/Tests/TestingTests/Support/CartesianProductTests.swift index b817b37f6..30b246a72 100644 --- a/Tests/TestingTests/Support/CartesianProductTests.swift +++ b/Tests/TestingTests/Support/CartesianProductTests.swift @@ -31,8 +31,9 @@ struct CartesianProductTests { // Test the size of the product is correct. let (c1, c2, product) = computeCartesianProduct() #expect(product.underestimatedCount == c1.underestimatedCount * c2.underestimatedCount) - #expect(Array(product).count == c1.count * c2.count) - #expect(Array(product).count == 26 * 100) + let productCount = Array(product).count + #expect(productCount == c1.count * c2.count) + #expect(productCount == 26 * 100) } @Test("First element is correct") @@ -58,8 +59,9 @@ struct CartesianProductTests { // NOTE: we need to break out the tuple elements because tuples aren't // directly equatable. - #expect(Array(product).map(\.0) == possibleValues.map(\.0)) - #expect(Array(product).map(\.1) == possibleValues.map(\.1)) + let productArray = Array(product) + #expect(productArray.map(\.0) == possibleValues.map(\.0)) + #expect(productArray.map(\.1) == possibleValues.map(\.1)) } @Test("Cartesian product with empty first input is empty") diff --git a/Tests/TestingTests/Traits/TimeLimitTraitTests.swift b/Tests/TestingTests/Traits/TimeLimitTraitTests.swift index 00c9cbb44..210d55e58 100644 --- a/Tests/TestingTests/Traits/TimeLimitTraitTests.swift +++ b/Tests/TestingTests/Traits/TimeLimitTraitTests.swift @@ -202,13 +202,12 @@ struct TimeLimitTraitTests { configuration.eventHandler = { event, context in guard case let .issueRecorded(issue) = event.kind, case .timeLimitExceeded = issue.kind, - let test = context.test, - context.testCase != nil - else { + let test = context.test else { return } issueRecorded() #expect(test.timeLimit == .milliseconds(10)) + #expect(context.testCase != nil) } await Test(.timeLimit(.milliseconds(10))) { diff --git a/Tests/TestingTests/VariadicGenericTests.swift b/Tests/TestingTests/VariadicGenericTests.swift index 0f54f6528..93d17612e 100644 --- a/Tests/TestingTests/VariadicGenericTests.swift +++ b/Tests/TestingTests/VariadicGenericTests.swift @@ -11,8 +11,59 @@ import Testing private import _TestingInternals -@Test func variadicCStringArguments() async throws { - #expect(swt_pointersNotEqual2("abc", "123")) - #expect(swt_pointersNotEqual3("abc", "123", "def")) - #expect(swt_pointersNotEqual4("abc", "123", "def", "456")) +@Test func stringsAsCStringArguments() { + let abc = "abc" + let _123 = "123" + let def = "def" + let _456 = "456" + #expect(0 == strcmp(abc, abc)) + #expect(0 != strcmp(abc, _123)) + #expect(swt_pointersNotEqual2(abc, _123)) + #expect(swt_pointersNotEqual3(abc, _123, def)) + #expect(swt_pointersNotEqual4(abc, _123, def, _456)) +} + +@Test func nilStringToCString() { + let nilString: String? = nil + #expect(swt_nullableCString(nilString) == false) +} + +@Test func inoutAsPointerPassedToCFunction() { + let num = CLong.random(in: 0 ..< 100) + let str = String(describing: num) + str.withCString { str in + var endptr: UnsafeMutablePointer? + #expect(num == strtol(str, &endptr, 10)) + #expect(endptr != nil) + #expect(endptr?.pointee == 0) + } +} + +@Test func utf16PointerConversions() throws { + _ = try withUnsafeTemporaryAllocation(of: UTF16.CodeUnit.self, capacity: 1) { buffer in + func f(_ p: UnsafeRawPointer?) -> Bool { true } + func g(_ p: UnsafeMutableRawPointer?) -> Bool { true } + func h(_ p: UnsafeMutablePointer?) -> Bool { true } + #expect(f(buffer.baseAddress)) + #expect(g(buffer.baseAddress)) + #expect(h(buffer.baseAddress)) + return try #require(String.decodeCString(buffer.baseAddress, as: UTF16.self)?.result) + } +} + +@Test func arrayAsCString() { + let array: [CChar] = Array("abc123".utf8.map(CChar.init(bitPattern:))) + #expect(0 == strcmp(array, "abc123")) +} + +@Test func arrayAsUTF16Pointer() { + let array: [UTF16.CodeUnit] = [1, 2, 3] + func f(_ p: UnsafePointer?) -> Bool { true } + #expect(f(array)) +} + +@Test func arrayAsNonBitwiseCopyablePointer() { + let array: [String] = ["a", "b", "c"] + func f(_ p: UnsafePointer?) -> Bool { true } + #expect(f(array)) }