diff --git a/Package.swift b/Package.swift index 4353a164b..f6695f528 100644 --- a/Package.swift +++ b/Package.swift @@ -177,6 +177,13 @@ let package = Package( path: "Tests/_MemorySafeTestingTests", swiftSettings: .packageSettings + [.strictMemorySafety()] ), + .testTarget( + name: "SubexpressionShowcase", + dependencies: [ + "Testing", + ], + swiftSettings: .packageSettings + ), .macro( name: "TestingMacros", @@ -384,6 +391,8 @@ extension Array where Element == PackageDescription.SwiftSetting { .enableUpcomingFeature("InferIsolatedConformances"), + .enableExperimentalFeature("Lifetimes"), + // When building as a package, the macro plugin always builds as an // executable rather than a library. .define("SWT_NO_LIBRARY_MACRO_PLUGINS"), diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedExpectation.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedExpectation.swift new file mode 100644 index 000000000..ad566664d --- /dev/null +++ b/Sources/Testing/ABI/Encoded/ABI.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 ABI { + /// 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 where V: ABI.Version { + /// 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 ABI.EncodedExpectation: Codable {} diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedExpression.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedExpression.swift new file mode 100644 index 000000000..653d0129e --- /dev/null +++ b/Sources/Testing/ABI/Encoded/ABI.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 ABI { + /// 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 where V: ABI.Version { + /// 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 ABI.EncodedExpression: Codable {} diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift index c593a68a5..ff5cbc501 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedIssue.swift @@ -58,6 +58,11 @@ extension ABI { /// - 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) { // >= v0 isKnown = issue.isKnown @@ -80,6 +85,9 @@ extension ABI { 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/Encoded/ABI.EncodedMessage.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedMessage.swift index 8f993ecb7..89e30a443 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedMessage.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedMessage.swift @@ -64,11 +64,19 @@ extension ABI { /// 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/Attachments/Attachment.swift b/Sources/Testing/Attachments/Attachment.swift index 59a88d729..1c87d8176 100644 --- a/Sources/Testing/Attachments/Attachment.swift +++ b/Sources/Testing/Attachments/Attachment.swift @@ -188,7 +188,7 @@ extension Attachment: CustomStringConvertible where AttachableValue: ~Copyable { /// @Available(Xcode, introduced: 26.0) /// } public var description: String { - if #available(_castingWithNonCopyableGenerics, *), let attachableValue = boxCopyableValue(attachableValue) { + if #available(_castingWithNonCopyableGenerics, *), let attachableValue = makeExistential(attachableValue) { return #""\#(preferredName)": \#(String(describingForTest: attachableValue))"# } let typeInfo = TypeInfo(describing: AttachableValue.self) diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index f22bf7638..b3802b0e7 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -18,6 +18,8 @@ add_library(Testing ABI/Encoded/ABI.EncodedBacktrace.swift ABI/Encoded/ABI.EncodedError.swift ABI/Encoded/ABI.EncodedEvent.swift + ABI/Encoded/ABI.EncodedExpectation.swift + ABI/Encoded/ABI.EncodedExpression.swift ABI/Encoded/ABI.EncodedInstant.swift ABI/Encoded/ABI.EncodedIssue.swift ABI/Encoded/ABI.EncodedMessage.swift @@ -48,6 +50,7 @@ add_library(Testing Expectations/Expectation.swift Expectations/Expectation+Macro.swift Expectations/ExpectationChecking+Macro.swift + Expectations/ExpectationContext.swift Issues/Confirmation.swift Issues/ErrorSnapshot.swift Issues/Issue.swift @@ -70,7 +73,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 80e68c609..3ed588096 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: @@ -171,7 +187,7 @@ extension Event.Symbol { case .attachment: return "\(_ansiEscapeCodePrefix)94m\(symbolCharacter)\(_resetANSIEscapeCode)" case .details: - return symbolCharacter + return "\(symbolCharacter)" } } return "\(symbolCharacter)" @@ -305,18 +321,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 @@ -325,17 +335,17 @@ extension Event.ConsoleOutputRecorder { // to the indentation provided by the symbol. var lines = message.stringValue.split(omittingEmptySubsequences: false, 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 0634ec904..b40e581e6 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 @@ -499,20 +508,18 @@ extension Event.HumanReadableOutputRecorder { additionalMessages.append(_formattedComment(knownIssueComment)) } - 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 f546660ff..556c9a5d2 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -458,17 +458,16 @@ extension ExitTest { /// This function contains the common implementation for all /// `await #expect(processExitsWith:) { }` invocations regardless of calling /// convention. -func callExitTest( +nonisolated(nonsending) func callExitTest( identifiedBy exitTestID: (UInt64, UInt64, UInt64, UInt64), encodingCapturedValues capturedValues: [ExitTest.CapturedValue], processExitsWith expectedExitCondition: ExitTest.Condition, observing observedValues: [any PartialKeyPath & Sendable], - expression: __Expression, + sourceCode: @escaping @autoclosure @Sendable () -> KeyValuePairs<__ExpressionID, String>, comments: @autoclosure () -> [Comment], isRequired: Bool, - isolation: isolated (any Actor)? = #isolation, sourceLocation: SourceLocation -) async -> Result { +) async -> Result { guard let configuration = Configuration.current ?? Configuration.all.first else { preconditionFailure("A test must be running on the current task to use #expect(processExitsWith:).") } @@ -521,11 +520,14 @@ func callExitTest( } // Plumb the exit test's result through the general expectation machinery. - let expression = __Expression(String(describingForTest: expectedExitCondition)) - return __checkValue( + let expectationContext = __ExpectationContext( + sourceCode: [.root: String(describingForTest: expectedExitCondition)], + runtimeValues: [.root: { Expression.Value(reflecting: result.exitStatus) }] + ) + return check( expectedExitCondition.isApproximatelyEqual(to: result.exitStatus), - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(result.exitStatus), + expectationContext: expectationContext, + mismatchedErrorDescription: nil, comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation diff --git a/Sources/Testing/Expectations/Expectation+Macro.swift b/Sources/Testing/Expectations/Expectation+Macro.swift index ea007f667..15da38e46 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. @@ -89,7 +89,7 @@ /// running in the current task and an instance of ``ExpectationFailedError`` is /// thrown. /// -/// This overload of ``require(_:_:sourceLocation:)-6w9oo`` checks if +/// This overload of ``require(_:_:sourceLocation:)-5l63q`` checks if /// `optionalValue` may be ambiguous (i.e. it is unclear if the developer /// intended to check for a boolean value or unwrap an optional boolean value) /// and provides additional compile-time diagnostics when it is. @@ -118,16 +118,16 @@ public macro require( /// running in the current task and an instance of ``ExpectationFailedError`` is /// thrown. /// -/// This overload of ``require(_:_:sourceLocation:)-6w9oo`` is used when a +/// This overload of ``require(_:_:sourceLocation:)-5l63q`` is used when a /// non-optional, non-`Bool` value is passed to `#require()`. It emits a warning /// diagnostic indicating that the expectation is redundant. @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..569c37942 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,14 +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? - - /// A description of the exit condition that was expected to be matched. - /// - /// If this expectation passed, the value of this property is `nil` because no - /// exit test failed. - @_spi(Experimental) @_spi(ForToolsIntegrationOnly) - public var mismatchedExitConditionDescription: String? + public var differenceDescription: String? { + evaluatedExpression.differenceDescription + } /// Whether the expectation passed or failed. /// @@ -100,7 +95,7 @@ extension Expectation { /// - Parameter expectation: The real expectation. public init(snapshotting expectation: borrowing Expectation) { self.evaluatedExpression = expectation.evaluatedExpression - self.mismatchedErrorDescription = expectation.mismatchedErrorDescription ?? expectation.mismatchedExitConditionDescription + self.mismatchedErrorDescription = expectation.mismatchedErrorDescription self.differenceDescription = expectation.differenceDescription self.isPassing = expectation.isPassing self.isRequired = expectation.isRequired diff --git a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift index 4d5de7761..dfd071dd2 100644 --- a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift +++ b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift @@ -13,13 +13,10 @@ /// /// - 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. /// - comments: An array of comments describing the expectation. This array /// may be empty. /// - isRequired: Whether or not the expectation is required. The value of @@ -27,9 +24,9 @@ /// failure. /// - sourceLocation: The source location of the expectation. /// -/// - Returns: A `Result`. If `condition` is `true`, the result -/// is `.success`. If `condition` is `false`, the result is an instance of -/// ``ExpectationFailedError`` describing the failure. +/// - Returns: A `Result`. If `condition` is +/// `true`, the result is `.success`. If `condition` is `false`, the result is +/// an instance of ``ExpectationFailedError`` describing the failure. /// /// If the condition evaluates to `false`, an ``Issue`` is recorded for the test /// that is running in the current task. @@ -55,47 +52,25 @@ /// 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?, 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) - } - } +) -> Result where T: ~Copyable { + 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,8 +83,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 // from the testing framework which aren't relevant to the user. @@ -120,704 +93,182 @@ 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) -} - -/// 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) -/// ``` -/// -/// - 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. -@_disfavoredOverload -public func __checkFunctionCall( - _ lhs: T, calling functionCall: (T, repeat each U) throws -> Bool, _ arguments: 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 - ) -} - -#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( - condition, - 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 -> Bool, _ argument0: Arg0, _ argument1: Arg1, - expression: __Expression, - 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 - ) -} - -/// 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 - ) -} +// MARK: - Expectation checks -/// 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 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 __checkFunctionCall( - _ lhs: T, calling functionCall: (T, Arg0, Arg1, Arg2, Arg3) throws -> Bool, _ argument0: Arg0, _ argument1: Arg1, _ argument2: Arg2, _ argument3: Arg3, - expression: __Expression, +public func __checkCondition( + _ condition: (__ExpectationContext) throws(E) -> Bool, + sourceCode: @escaping @autoclosure @Sendable () -> KeyValuePairs<__ExpressionID, String>, 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 +) throws(E) -> Result { + let expectationContext = __ExpectationContext(sourceCode: sourceCode()) + let condition = try condition(expectationContext) -/// 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( + return check( 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. -@_disfavoredOverload -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), + expectationContext: expectationContext, + mismatchedErrorDescription: nil, 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")`.) +/// 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) throws -> R?, _ argument0: Arg0, - expression: __Expression, +public func __checkCondition( + _ optionalValue: (__ExpectationContext) throws(E) -> T?, + sourceCode: @escaping @autoclosure @Sendable () -> KeyValuePairs<__ExpressionID, String>, 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 - ) -} +) throws(E) -> Result where T: ~Copyable { + let 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) 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, 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 nonisolated(nonsending) func __checkConditionAsync( + _ condition: (__ExpectationContext) async throws(E) -> Bool, + sourceCode: @escaping @autoclosure @Sendable () -> KeyValuePairs<__ExpressionID, String>, 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 - ) -} - -// MARK: - Property access +) async throws(E) -> Result { + let expectationContext = __ExpectationContext(sourceCode: sourceCode()) + let condition = try await condition(expectationContext) -/// 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, 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 nonisolated(nonsending) func __checkConditionAsync( + _ optionalValue: (__ExpectationContext) async throws(E) -> sending T?, + sourceCode: @escaping @autoclosure @Sendable () -> KeyValuePairs<__ExpressionID, String>, comments: @autoclosure () -> [Comment], isRequired: Bool, sourceLocation: SourceLocation -) -> Result { - let optionalValue = memberAccess(lhs) - return __checkValue( - optionalValue, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, optionalValue as U??), +) async throws(E) -> Result where T: ~Copyable { + let expectationContext = __ExpectationContext(sourceCode: sourceCode()) + let optionalValue = try await optionalValue(expectationContext) + + let result = check( + optionalValue != nil, + expectationContext: expectationContext, + mismatchedErrorDescription: 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. -/// -/// - 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, - 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 - ) -} +// MARK: - "Escape hatch" expectation checks -/// 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. +/// 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: T, _ op: (T, () -> U) -> Bool, _ rhs: @autoclosure () -> U, - expression: __Expression, +public func __checkEscapedCondition( + _ condition: Bool, + sourceCode: @escaping @autoclosure @Sendable () -> KeyValuePairs<__ExpressionID, String>, comments: @autoclosure () -> [Comment], isRequired: Bool, sourceLocation: SourceLocation -) -> Result where T: RangeExpression, U: RangeExpression { - let (condition, rhs) = _callBinaryOperator(lhs, op, rhs) - return __checkValue( +) -> Result { + let expectationContext = __ExpectationContext(sourceCode: sourceCode()) + + return check( condition, - expression: expression, - expressionWithCapturedRuntimeValues: expression.capturingRuntimeValues(lhs, rhs), - difference: nil, + expectationContext: expectationContext, + mismatchedErrorDescription: 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 () -> KeyValuePairs<__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 - ) -} +) -> Result where T: ~Copyable { + let expectationContext = __ExpectationContext(sourceCode: sourceCode()) -// MARK: - Optional unwrapping - -/// 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, 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) } } @@ -834,16 +285,16 @@ public func __checkCast( public func __checkClosureCall( throws errorType: E.Type, performing body: () throws -> some Any, - expression: __Expression, + sourceCode: @escaping @autoclosure @Sendable () -> KeyValuePairs<__ExpressionID, String>, comments: @autoclosure () -> [Comment], isRequired: Bool, sourceLocation: SourceLocation -) -> Result where E: Error { +) -> Result where E: Error { if errorType == Never.self { __checkClosureCall( throws: Never.self, performing: body, - expression: expression, + sourceCode: sourceCode(), comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation @@ -853,7 +304,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 @@ -869,23 +320,21 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public nonisolated(nonsending) func __checkClosureCall( throws errorType: E.Type, performing body: () async throws -> sending some Any, - expression: __Expression, + sourceCode: @escaping @autoclosure @Sendable () -> KeyValuePairs<__ExpressionID, String>, comments: @autoclosure () -> [Comment], isRequired: Bool, - isolation: isolated (any Actor)? = #isolation, sourceLocation: SourceLocation -) async -> Result where E: Error { +) async -> Result where E: Error { if errorType == Never.self { await __checkClosureCall( throws: Never.self, performing: body, - expression: expression, + sourceCode: sourceCode(), comments: comments(), isRequired: isRequired, - isolation: isolation, sourceLocation: sourceLocation ).map { _ in nil } } else { @@ -893,10 +342,9 @@ 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, sourceLocation: sourceLocation ).map { $0 as? E } } @@ -915,11 +363,11 @@ public func __checkClosureCall( public func __checkClosureCall( throws _: Never.Type, performing body: () throws -> some Any, - expression: __Expression, + sourceCode: @escaping @autoclosure @Sendable () -> KeyValuePairs<__ExpressionID, String>, comments: @autoclosure () -> [Comment], isRequired: Bool, sourceLocation: SourceLocation -) -> Result { +) -> Result { var success = true var mismatchExplanationValue: String? = nil do { @@ -929,9 +377,10 @@ 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, comments: comments(), isRequired: isRequired, @@ -948,15 +397,14 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public nonisolated(nonsending) func __checkClosureCall( throws _: Never.Type, performing body: () async throws -> sending some Any, - expression: __Expression, + sourceCode: @escaping @autoclosure @Sendable () -> KeyValuePairs<__ExpressionID, String>, comments: @autoclosure () -> [Comment], isRequired: Bool, - isolation: isolated (any Actor)? = #isolation, sourceLocation: SourceLocation -) async -> Result { +) async -> Result { var success = true var mismatchExplanationValue: String? = nil do { @@ -966,9 +414,10 @@ 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, comments: comments(), isRequired: isRequired, @@ -988,16 +437,16 @@ public func __checkClosureCall( public func __checkClosureCall( throws error: E, performing body: () throws -> some Any, - expression: __Expression, + sourceCode: @escaping @autoclosure @Sendable () -> KeyValuePairs<__ExpressionID, String>, comments: @autoclosure () -> [Comment], isRequired: Bool, sourceLocation: SourceLocation -) -> Result where E: Error & Equatable { +) -> Result where E: Error & Equatable { __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 @@ -1011,23 +460,21 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public nonisolated(nonsending) func __checkClosureCall( throws error: E, performing body: () async throws -> sending some Any, - expression: __Expression, + sourceCode: @escaping @autoclosure @Sendable () -> KeyValuePairs<__ExpressionID, String>, comments: @autoclosure () -> [Comment], isRequired: Bool, - isolation: isolated (any Actor)? = #isolation, sourceLocation: SourceLocation -) async -> Result where E: Error & Equatable { +) async -> Result where E: Error & Equatable { await __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, sourceLocation: sourceLocation ).map { $0 as? E } } @@ -1044,14 +491,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 () -> KeyValuePairs<__ExpressionID, String>, comments: @autoclosure () -> [Comment], isRequired: Bool, sourceLocation: SourceLocation -) -> Result<(any Error)?, any Error> { +) -> Result<(any Error)?, ExpectationFailedError> { + let expectationContext = __ExpectationContext<(any Error)?>(sourceCode: sourceCode()) + var errorMatches = false var mismatchExplanationValue: String? = nil - var expression = expression var caughtError: (any Error)? do { let result = try body() @@ -1063,7 +511,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) } @@ -1074,9 +522,9 @@ public func __checkClosureCall( } } - return __checkValue( + return check( errorMatches, - expression: expression, + expectationContext: expectationContext, mismatchedErrorDescription: mismatchExplanationValue, comments: comments(), isRequired: isRequired, @@ -1090,19 +538,19 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public nonisolated(nonsending) 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 () -> KeyValuePairs<__ExpressionID, String>, comments: @autoclosure () -> [Comment], isRequired: Bool, - isolation: isolated (any Actor)? = #isolation, sourceLocation: SourceLocation -) async -> Result<(any Error)?, any Error> { +) async -> Result<(any Error)?, ExpectationFailedError> { + let expectationContext = __ExpectationContext<(any Error)?>(sourceCode: sourceCode()) + var errorMatches = false var mismatchExplanationValue: String? = nil - var expression = expression var caughtError: (any Error)? do { let result = try await body() @@ -1114,7 +562,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) } @@ -1125,9 +573,9 @@ public func __checkClosureCall( } } - return __checkValue( + return check( errorMatches, - expression: expression, + expectationContext: expectationContext, mismatchedErrorDescription: mismatchExplanationValue, comments: comments(), isRequired: isRequired, @@ -1146,23 +594,22 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public nonisolated(nonsending) func __checkClosureCall( identifiedBy exitTestID: (UInt64, UInt64, UInt64, UInt64), processExitsWith expectedExitCondition: ExitTest.Condition, observing observedValues: [any PartialKeyPath & Sendable] = [], performing _: @convention(c) () -> Void, - expression: __Expression, + sourceCode: @escaping @autoclosure @Sendable () -> KeyValuePairs<__ExpressionID, String>, comments: @autoclosure () -> [Comment], isRequired: Bool, - isolation: isolated (any Actor)? = #isolation, sourceLocation: SourceLocation -) async -> Result { +) async -> Result { await callExitTest( identifiedBy: exitTestID, encodingCapturedValues: [], processExitsWith: expectedExitCondition, observing: observedValues, - expression: expression, + sourceCode: sourceCode(), comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation @@ -1177,24 +624,23 @@ public func __checkClosureCall( /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. -public func __checkClosureCall( +public nonisolated(nonsending) func __checkClosureCall( identifiedBy exitTestID: (UInt64, UInt64, UInt64, UInt64), encodingCapturedValues capturedValues: (repeat each T), processExitsWith expectedExitCondition: ExitTest.Condition, observing observedValues: [any PartialKeyPath & Sendable] = [], performing _: @convention(c) () -> Void, - expression: __Expression, + sourceCode: @escaping @autoclosure @Sendable () -> KeyValuePairs<__ExpressionID, String>, comments: @autoclosure () -> [Comment], isRequired: Bool, - isolation: isolated (any Actor)? = #isolation, sourceLocation: SourceLocation -) async -> Result where repeat each T: Codable & Sendable { +) async -> Result where repeat each T: Codable & Sendable { await callExitTest( identifiedBy: exitTestID, encodingCapturedValues: Array(repeat each capturedValues), processExitsWith: expectedExitCondition, observing: observedValues, - expression: expression, + sourceCode: sourceCode(), comments: comments(), isRequired: isRequired, sourceLocation: sourceLocation diff --git a/Sources/Testing/Expectations/ExpectationContext.swift b/Sources/Testing/Expectations/ExpectationContext.swift new file mode 100644 index 000000000..1760fe384 --- /dev/null +++ b/Sources/Testing/Expectations/ExpectationContext.swift @@ -0,0 +1,405 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024–2026 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. +/// +/// Instances of this type do _not_ conform to [`Sendable`](https://developer.apple.com/documentation/swift/sendable) +/// because they may capture non-sendable state generated during the evaluation +/// of an expression. +/// +/// - Warning: This type is used to implement the `#expect()` and `#require()` +/// macros. Do not use it directly. +public final class __ExpectationContext where Output: ~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 () -> KeyValuePairs<__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?] + + init( + sourceCode: @escaping @autoclosure @Sendable () -> KeyValuePairs<__ExpressionID, String> = [:], + runtimeValues: [__ExpressionID: () -> Expression.Value?] = [:], + differences: [__ExpressionID: () -> CollectionDifference?] = [:] + ) { + _sourceCode = sourceCode + self.runtimeValues = runtimeValues + self.differences = differences + } + + /// 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 where Output: ~Copyable {} + +// MARK: - Expression capturing + +extension __ExpectationContext where Output: ~Copyable { + /// 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 subscript overloads disambiguate themselves and avoid + /// accidental recursion. + func captureValue(_ value: borrowing T, _ id: __ExpressionID) where T: ~Copyable & ~Escapable { + if #available(_castingWithNonCopyableGenerics, *), let value = makeExistential(value) { + runtimeValues[id] = { Expression.Value(reflecting: value) } + } else { + runtimeValues[id] = { Expression.Value(failingToReflectInstanceOf: T.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. + /// + /// - Warning: This function is used to implement the `#expect()` and + /// `#require()` macros. Do not call it directly. + @_lifetime(borrow value) + public func callAsFunction(_ value: borrowing T, _ id: __ExpressionID) -> T where T: ~Escapable { + captureValue(value, id) + return copy value + } + + /// 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 func __inoutAfter(_ value: inout T, _ id: __ExpressionID) where T: ~Copyable & ~Escapable { + captureValue(value, id) + } +} + +// MARK: - Collection comparison and diffing + +extension __ExpectationContext where Output: ~Copyable { + /// 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: ", ") + } + + /// Capture the difference between `lhs` and `rhs` at runtime. + /// + /// - Parameters: + /// - lhs: The left-hand operand. + /// - rhs: The right-hand operand. + /// - opID: A value that uniquely identifies the binary operation expression + /// of which `lhs` and `rhs` are operands. + /// + /// This function performs additional type checking of `lhs` and `rhs` at + /// runtime. If we instead overload the caller (`__cmp()`) it puts extra + /// compile-time pressure on the type checker that we don't want. + func captureDifferences(_ lhs: T, _ rhs: U, _ opID: __ExpressionID) { +#if !hasFeature(Embedded) // no existentials + if let lhs = lhs as? any StringProtocol { + func open(_ lhs: V, _ rhs: U) where V: StringProtocol { + guard let rhs = rhs as? V else { + return + } + 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) + } + } + open(lhs, rhs) + } else if lhs is any RangeExpression { + // Do _not_ perform a diffing operation on `lhs` and `rhs`. Range + // expressions are not usefully diffable the way other kinds of + // collections are. SEE: https://github.com/swiftlang/swift-testing/issues/639 + } else if let lhs = lhs as? any BidirectionalCollection { + func open(_ lhs: V, _ rhs: U) where V: BidirectionalCollection { + guard let rhs = rhs as? V, + let elementType = V.Element.self as? any Equatable.Type else { + return + } + differences[opID] = { + func open(_: E.Type) -> CollectionDifference where E: Equatable { + let lhs: some BidirectionalCollection = lhs.lazy.map { $0 as! E } + let rhs: some BidirectionalCollection = rhs.lazy.map { $0 as! E } + return CollectionDifference(lhs.difference(from: rhs)) + } + return open(elementType) + } + } + open(lhs, rhs) + } +#endif + } + + /// 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 right-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)`. + /// + /// - Warning: This function is used to implement the `#expect()` and + /// `#require()` macros. Do not call it directly. + public func __cmp( + _ op: (borrowing T, borrowing U) throws(E) -> Bool, + _ opID: __ExpressionID, + _ lhs: borrowing T, + _ lhsID: __ExpressionID, + _ rhs: borrowing U, + _ rhsID: __ExpressionID + ) throws(E) -> Bool where T: ~Copyable, U: ~Copyable { + captureValue(lhs, lhsID) + captureValue(rhs, rhsID) + + let result = try op(lhs, rhs) + captureValue(result, opID) + + if !result, + #available(_castingWithNonCopyableGenerics, *), + let lhs = makeExistential(lhs), + let rhs = makeExistential(rhs) { + captureDifferences(lhs, rhs, opID) + } + + return result + } +} + +// MARK: - Casting + +extension __ExpectationContext where Output: ~Copyable { + /// 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. + public func __as(_ value: borrowing T, _ valueID: __ExpressionID, _ type: U.Type, _ typeID: __ExpressionID) -> U? { + let value = copy value + + captureValue(value, valueID) + let result = value 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. + public func __is(_ value: borrowing T, _ valueID: __ExpressionID, _ type: U.Type, _ typeID: __ExpressionID) -> Bool { + __as(value, valueID, type, typeID) != nil + } +} diff --git a/Sources/Testing/Issues/Issue+Recording.swift b/Sources/Testing/Issues/Issue+Recording.swift index 72f4c65a4..97365fc81 100644 --- a/Sources/Testing/Issues/Issue+Recording.swift +++ b/Sources/Testing/Issues/Issue+Recording.swift @@ -212,11 +212,10 @@ extension Issue { /// - Returns: The issue representing the caught error, if any error was /// caught, otherwise `nil`. @discardableResult - static func withErrorRecording( + nonisolated(nonsending) static func withErrorRecording( at sourceLocation: SourceLocation?, configuration: Configuration? = nil, - isolation: isolated (any Actor)? = #isolation, - _ body: () async throws -> Void + _ body: nonisolated(nonsending) () async throws -> Void ) async -> (any Error)? { // Ensure that we are capturing backtraces for errors before we start // expecting to see them. diff --git a/Sources/Testing/Issues/Issue.swift b/Sources/Testing/Issues/Issue.swift index 8b05cb424..dc7e2bc54 100644 --- a/Sources/Testing/Issues/Issue.swift +++ b/Sources/Testing/Issues/Issue.swift @@ -284,8 +284,6 @@ extension Issue.Kind: CustomStringConvertible { case let .expectationFailed(expectation): return if let mismatchedErrorDescription = expectation.mismatchedErrorDescription { "Expectation failed: \(mismatchedErrorDescription)" - } else if let mismatchedExitConditionDescription = expectation.mismatchedExitConditionDescription { - "Expectation failed: \(mismatchedExitConditionDescription)" } else { "Expectation failed: \(expectation.evaluatedExpression.expandedDescription())" } 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 a294a81e0..cee7afd63 100644 --- a/Sources/Testing/SourceAttribution/Expression.swift +++ b/Sources/Testing/SourceAttribution/Expression.swift @@ -35,64 +35,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. @@ -106,30 +48,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)" } } @@ -310,6 +230,20 @@ public struct __Expression: Sendable { self.children = children } } + + /// Initialize an instance of this type representing a value of the + /// specified type that could not be captured (due to e.g. not conforming to + /// `Copyable`.) + /// + /// - Parameters: + /// - type: The type of the uncaptured value. + init?(failingToReflectInstanceOf type: T.Type) where T: ~Copyable & ~Escapable { + let typeInfo = TypeInfo(describing: type) + self.description = "" + self.debugDescription = "" + self.typeInfo = typeInfo + self.isCollection = false + } } /// A representation of the runtime value of this expression. @@ -319,235 +253,66 @@ 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 - } + /// This function provides the implementation of ``expandedDescription()`` + /// with additional options used by ``Event/HumanReadableOutputRecorder``. + func expandedDescription(verbose: Bool) -> String { + var result = sourceCode - 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)" - } - - // 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 { + 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 } } @@ -555,7 +320,6 @@ public struct __Expression: Sendable { extension __Expression: Codable {} extension __Expression.Kind: Codable {} -extension __Expression.Kind.FunctionCallArgument: Codable {} extension __Expression.Value: Codable {} // MARK: - CustomStringConvertible, CustomDebugStringConvertible diff --git a/Sources/Testing/SourceAttribution/ExpressionID.swift b/Sources/Testing/SourceAttribution/ExpressionID.swift new file mode 100644 index 000000000..a362982e7 --- /dev/null +++ b/Sources/Testing/SourceAttribution/ExpressionID.swift @@ -0,0 +1,175 @@ +// +// 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 +// + +private import _TestingInternals + +/// 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 128 bits or fewer (64 or fewer on Apple platforms +/// due to ABI constraints). Wider unique identifiers are stored as arrays of +/// integers. 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. + /// + /// Instances of this type can be used to produce keys and key paths for an + /// instance of `Graph` whose key type is `UInt32`. + 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: IntegerLiteralType) + + /// This ID contains key path elements that do not fit in an instance of + /// ``IntegerLiteralType``. The elements 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.Elements: Collection { + var count: Int { + switch self { + case .none: + 0 + case let .packed(word): + word.nonzeroBitCount + case let .keyPath(keyPath): + keyPath.count + } + } + + var startIndex: Int { + switch self { + case .none, .keyPath: + 0 + case let .packed(word): + word.trailingZeroBitCount + } + } + + var endIndex: Int { + switch self { + case .none: + 0 + case .packed: + __ExpressionID.IntegerLiteralType.bitWidth + case let .keyPath(keyPath): + keyPath.count + } + } + + func index(after i: Int) -> Int { + let uncheckedNextIndex = i + 1 + switch self { + 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 self { + case .none: + swt_unreachable() + case .packed: + UInt32(position) + case let .keyPath(keyPath): + keyPath[position] + } + } +} + +extension __ExpressionID { + /// 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 { + 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 { +#if SWT_TARGET_OS_APPLE + public typealias IntegerLiteralType = UInt64 +#else + public typealias IntegerLiteralType = UInt128 +#endif + + public init(integerLiteral: IntegerLiteralType) { + 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/CopyableAdditions.swift b/Sources/Testing/Support/Additions/CopyableAdditions.swift index ea192b1ef..d0d32b435 100644 --- a/Sources/Testing/Support/Additions/CopyableAdditions.swift +++ b/Sources/Testing/Support/Additions/CopyableAdditions.swift @@ -9,22 +9,29 @@ // #if !hasFeature(Embedded) -/// A helper protocol for ``boxCopyableValue(_:)``. -private protocol _CopyablePointer { +/// A helper protocol for ``makeExistential(_:)``. +private protocol _CopierProtocol { + /// The type of value that a conforming type can copy. + associatedtype Referent + /// Load the value at this address into an existential box. /// /// - Returns: The value at this address. - func load() -> Any + static func load(from value: Referent) -> Any? } -extension UnsafePointer: _CopyablePointer where Pointee: Copyable { - func load() -> Any { - pointee +/// A helper type for ``makeExistential(_:)`` +private struct _Copier where Referent: ~Copyable & ~Escapable {} + +extension _Copier: _CopierProtocol where Referent: Copyable & Escapable { + static func load(from value: Referent) -> Any? { + value } } #endif -/// Copy a value to an existential box if its type conforms to `Copyable`. +/// Copy a value to an existential box if its type conforms to `Copyable` and +/// `Escapable`. /// /// - Parameters: /// - value: The value to copy. @@ -35,13 +42,14 @@ extension UnsafePointer: _CopyablePointer where Pointee: Copyable { /// When using Embedded Swift, this function always returns `nil`. #if !hasFeature(Embedded) @available(_castingWithNonCopyableGenerics, *) -func boxCopyableValue(_ value: borrowing some ~Copyable) -> Any? { - withUnsafePointer(to: value) { address in - return (address as? any _CopyablePointer)?.load() +func makeExistential(_ value: borrowing T) -> Any? where T: ~Copyable & ~Escapable { + if let type = _Copier.self as? any _CopierProtocol.Type { + return type.load(from: value) } + return nil } #else -func boxCopyableValue(_ value: borrowing some ~Copyable) -> Void? { +func makeExistential(_ value: borrowing T) -> Void? where T: ~Copyable & ~Escapable { nil } #endif 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/Testing/Support/Graph.swift b/Sources/Testing/Support/Graph.swift index 741efe06c..226e953f0 100644 --- a/Sources/Testing/Support/Graph.swift +++ b/Sources/Testing/Support/Graph.swift @@ -159,6 +159,11 @@ struct Graph where K: Hashable { // indicate there was no previous value (otherwise we'll end up // returning the value we just created.) var child = Graph(value: intermediateValue) + func f() -> Any { + let start = keyPath.index(keyPath.startIndex, offsetBy: 1, limitedBy: keyPath.endIndex) ?? keyPath.endIndex + return keyPath[start.. = [] + var effectKeywordsToApplyOverall: Set = [] do { if let trailingClosureIndex { + expandedFunctionName = .identifier("__checkClosureCall") + // Include all arguments other than the "comment" and "sourceLocation" // arguments here. checkArguments += macroArguments.indices.lazy @@ -123,17 +142,90 @@ 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, in: context) + ) + ) - expandedFunctionName = .identifier("__checkClosureCall") + } else if let firstArgument = macroArguments.first { + let originalArgumentExpr = firstArgument.expression + let effectKeywordsFromNode = findEffectKeywords(in: originalArgumentExpr) + let effectKeywordsFromLexicalContext = findEffectKeywords(in: context) + effectKeywordsToApply = effectKeywordsFromNode.union(effectKeywordsFromLexicalContext) + effectKeywordsToApplyOverall = effectKeywordsToApply.subtracting(effectKeywordsFromLexicalContext) + + var useEscapeHatch = false + if let asExpr = (removeParentheses(from: originalArgumentExpr) ?? 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 + } - } 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 + if !useEscapeHatch { + if effectKeywordsToApply.contains(.await) { + expandedFunctionName = .identifier("__checkConditionAsync") + } + + let expectationContextName = context.makeUniqueClosureParameterName("__ec", in: originalArgumentExpr) + let resultOfRewrite = rewrite( + originalArgumentExpr, + usingExpectationContextNamed: expectationContextName, + for: macro, + rootedAt: originalArgumentExpr, + effectKeywordsToApply: effectKeywordsToApply, + returnType: returnType, + in: context + ) + if let (closureExpr, rewrittenNodes) = resultOfRewrite { + checkArguments.append(Argument(expression: closureExpr)) + + checkArguments.append( + Argument( + label: "sourceCode", + expression: createDictionaryExpr( + forSourceCodeOf: rewrittenNodes, + rootedAt: originalArgumentExpr, + in: context + ) + ) + ) + } else { + // The rewrite was cancelled, so switch to the escape hatch. + useEscapeHatch = true + } + } + + if useEscapeHatch { + expandedFunctionName = .identifier("__checkEscapedCondition") + + checkArguments.append(firstArgument) + checkArguments.append( + Argument( + label: "sourceCode", + expression: createDictionaryExpr(forSourceCodeOf: originalArgumentExpr, in: context) + ) + ) + } // Include all arguments other than the "condition", "comment", and // "sourceLocation" arguments here. @@ -142,15 +234,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, preceding the @@ -197,11 +280,15 @@ 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()" } - return "\(call).__expected()" + + call = applyEffectfulKeywords(effectKeywordsToApplyOverall, to: call, insertThunkCalls: false) + return call } /// Get the complete argument list for a given macro, including any trailing @@ -265,6 +352,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 { @@ -275,6 +381,10 @@ extension RefinedConditionMacro { public static var isThrowing: Bool { Base.isThrowing } + + public static var returnType: TypeSyntax? { + Base.returnType + } } // MARK: - Diagnostics-emitting condition macros @@ -284,7 +394,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, @@ -295,7 +405,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 @@ -327,7 +437,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, @@ -355,7 +465,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) } } @@ -384,7 +494,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) } } @@ -404,7 +514,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) } } diff --git a/Sources/TestingMacros/Support/Additions/MacroExpansionContextAdditions.swift b/Sources/TestingMacros/Support/Additions/MacroExpansionContextAdditions.swift index d0f296892..f93923963 100644 --- a/Sources/TestingMacros/Support/Additions/MacroExpansionContextAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/MacroExpansionContextAdditions.swift @@ -8,10 +8,12 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // +import SwiftIfConfig import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros import SwiftDiagnostics +import SwiftParser extension MacroExpansionContext { /// Get the type of the given lexical context. @@ -87,6 +89,33 @@ extension MacroExpansionContext { return makeUniqueName("\(prefix)\(suffix)") } + + /// Generate a unique name for use in the macro as a closure parameter. + /// + /// - Parameters: + /// - name: The name to use as a basis for the uniquely-generated name. + /// - node: A syntax node within which `name` must be unique. + /// + /// - Returns: an identifier token containing a unique name suitable for use + /// as a closure parameter. + func makeUniqueClosureParameterName(_ name: String, in node: some SyntaxProtocol) -> TokenSyntax { + precondition(name.isValidSwiftIdentifier(for: .variableName)) + var result = TokenSyntax.identifier(name) + + func isNameUsed(_ name: TokenSyntax) -> Bool { + node.tokens(viewMode: .sourceAccurate).lazy + .map(\.tokenKind) + .contains(name.tokenKind) + } + + var suffix = 0 + while isNameUsed(result) { + defer { suffix += 1 } + result = .identifier("\(name)\(suffix)") + } + + return result + } } // MARK: - @@ -152,3 +181,12 @@ extension MacroExpansionContext { diagnose(DiagnosticMessage(syntax: Syntax(node), message: String(describing: message), severity: .warning)) } } + +// MARK: - Build configuration + +extension BuildConfiguration { + /// Whether or not the target OS appears to be a Darwin-based (i.e. Apple) OS. + func isDarwinTargetOS() throws -> Bool { + try isActiveTargetRuntime(name: "_ObjC") || isActiveTargetObjectFormat(name: "MachO") + } +} diff --git a/Sources/TestingMacros/Support/Additions/SyntaxProtocolAdditions.swift b/Sources/TestingMacros/Support/Additions/SyntaxProtocolAdditions.swift new file mode 100644 index 000000000..402c5fa3e --- /dev/null +++ b/Sources/TestingMacros/Support/Additions/SyntaxProtocolAdditions.swift @@ -0,0 +1,98 @@ +// +// 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 SwiftIfConfig +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +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. + /// - context: The macro context in which the expression is being parsed. + /// + /// - 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, in context: some MacroExpansionContext) -> 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[..(as _: T.Type) -> ExprSyntax where T: UnsignedInteger & FixedWidthInteger { + let maxID = ancestralNodeIDs.max() ?? 0 + if maxID < T.bitWidth { + // Pack all the node IDs into a single integer value. + let word = ancestralNodeIDs.reduce(into: T(0)) { word, id in + word |= (1 << id) + } + return ExprSyntax(IntegerLiteralExprSyntax(word, radix: .hex)) + + } else { + // Some ID exceeds what we can fit in a single literal, so just produce an + // array of node IDs instead. + return ExprSyntax( + FunctionCallExprSyntax( + calledExpression: TypeExprSyntax( + type: MemberTypeSyntax( + baseType: IdentifierTypeSyntax(name: .identifier("Testing")), + name: .identifier("__ExpressionID") + ) + ), + leftParen: .leftParenToken(), + rightParen: .rightParenToken() + ) { + for nodeID in ancestralNodeIDs { + LabeledExprSyntax(expression: IntegerLiteralExprSyntax(nodeID)) + } + } + ) + } + } + + let isDarwinTargetOS = try? context.buildConfiguration?.isDarwinTargetOS() + if isDarwinTargetOS == false, #available(macOS 15.0, *) { + // The target OS doesn't have OS deployment concerns for Swift types, so + // we ought to be able to use UInt128 consistently. (Remember: the + // #available check is run on the _host_, not the _target_!) + return makeExpr(as: UInt128.self) + } + // No build configuration available, threw an error checking the target OS, + // or on an older host macOS. Be conservative and use UInt64. + return makeExpr(as: UInt64.self) + } +} diff --git a/Sources/TestingMacros/Support/Additions/TokenSyntaxAdditions.swift b/Sources/TestingMacros/Support/Additions/TokenSyntaxAdditions.swift index 26b9d1923..c82fbf0d6 100644 --- a/Sources/TestingMacros/Support/Additions/TokenSyntaxAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/TokenSyntaxAdditions.swift @@ -41,6 +41,16 @@ extension TokenSyntax { return nil } + + /// Whether this instance represents some kind of operator. + var isOperator: Bool { + switch tokenKind { + case .binaryOperator, .prefixOperator, .postfixOperator: + return true + default: + return false + } + } } /// The `static` keyword, if `typeName` is not `nil`. diff --git a/Sources/TestingMacros/Support/ConditionArgumentParsing.swift b/Sources/TestingMacros/Support/ConditionArgumentParsing.swift index ed2aadb0f..bfb6c3054 100644 --- a/Sources/TestingMacros/Support/ConditionArgumentParsing.swift +++ b/Sources/TestingMacros/Support/ConditionArgumentParsing.swift @@ -12,44 +12,6 @@ import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros -/// The result of parsing the condition argument passed to `#expect()` or -/// `#require()`. -struct Condition { - /// The name of the function to call in the macro expansion (e.g. - /// `__check()`.) - var expandedFunctionName: TokenSyntax - - /// The condition as one or more arguments to the evaluation function, - /// suitable for passing as partial arguments to a call to `__check()`. - var arguments: [Argument] - - /// The condition's source code as an expression that produces an instance of - /// the testing library's `__Expression` type. - var expression: ExprSyntax - - init(_ expandedFunctionName: String, arguments: [Argument], expression: ExprSyntax) { - self.expandedFunctionName = .identifier(expandedFunctionName) - self.arguments = arguments - self.expression = expression - } - - /// Initialize an instance of this type representing a single expression (i.e. - /// one that could not be broken down further.) - /// - /// - Parameters: - /// - expr: The expression. - /// - expressionNode: The node from which to derive the `expression` - /// property. If `nil`, `expr` is used. - init(expression expr: some ExprSyntaxProtocol, expressionNode: Syntax? = nil) { - let expressionNode: Syntax = expressionNode ?? Syntax(expr) - self.init( - "__checkValue", - arguments: [Argument(expression: expr)], - expression: createExpressionExpr(from: expressionNode) - ) - } -} - /// Emit a diagnostic if an expression resolves to a trivial boolean literal /// (e.g. `#expect(true)`.) /// @@ -70,7 +32,7 @@ private func _diagnoseTrivialBooleanValue(from expr: ExprSyntax, for macro: some default: break } - } else if let literal = _negatedExpression(expr)?.0.as(BooleanLiteralExprSyntax.self) { + } else if let literal = _negatedExpression(expr)?.as(BooleanLiteralExprSyntax.self) { // This expression is of the form !true or !false. switch literal.literal.tokenKind { case .keyword(.true): @@ -93,14 +55,14 @@ private func _diagnoseTrivialBooleanValue(from expr: ExprSyntax, for macro: some /// negation expression. /// /// This function handles expressions such as `!foo` or `!(bar)`. -private func _negatedExpression(_ expr: ExprSyntax) -> (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 } } @@ -119,7 +81,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 @@ -128,411 +90,805 @@ 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 expectationContextNameExpr: 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]() + + /// Whether or not the entire operation was cancelled mid-flight (e.g. due to + /// encountering an expression that we cannot expand.) + var isCancelled = false + + init(in context: C, for macro: M, rootedAt effectiveRootNode: Syntax, expectationContextName: TokenSyntax) { + self.context = context + self.macro = macro + self.effectiveRootNode = effectiveRootNode + self.expectationContextNameExpr = DeclReferenceExprSyntax(baseName: expectationContextName.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 _n_ 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)) + + let calledExpr: ExprSyntax = if let functionName { + ExprSyntax(MemberAccessExprSyntax(base: expectationContextNameExpr, name: functionName)) + } else { + ExprSyntax(expectationContextNameExpr) + } + + var result = ExprSyntax( + FunctionCallExprSyntax( + calledExpression: calledExpr, + leftParen: .leftParenToken(), + rightParen: .rightParenToken() + ) { + LabeledExprSyntax(expression: node.trimmed) + LabeledExprSyntax(expression: originalNode.expressionID(rootedAt: effectiveRootNode, in: context)) + for argument in additionalArguments { + LabeledExprSyntax(argument) + } + } + ) + + // 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 { + result = ExprSyntax(OptionalChainingExprSyntax(expression: result)) + } + + default: + break + } + + result.leadingTrivia = originalNode.leadingTrivia + result.trailingTrivia = originalNode.trailingTrivia - // 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)) + return 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)") + // 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 node.argumentNames == nil, + let memberAccessExpr = node.parent?.as(MemberAccessExprSyntax.self), + ExprSyntax(node) == memberAccessExpr.base, + let functionCallExpr = memberAccessExpr.parent?.as(FunctionCallExprSyntax.self), + ExprSyntax(memberAccessExpr) == functionCallExpr.calledExpression { + // If the base name is an identifier and its first character is uppercase, + // it is presumably a type name or module name, so don't expand it. (This + // isn't a great heuristic, but it hopefully minimizes the module name + // problem above.) + if case .identifier = node.baseName.tokenKind, + let firstCharacter = node.baseName.textWithoutBackticks.first, firstCharacter.isUppercase { + return ExprSyntax(node) } - return Argument(label: argument.label, expression: "$\(raw: index + 1)") + + 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 ) - } 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: SubscriptCallExprSyntax) -> ExprSyntax { + _rewrite( + node + .with(\.calledExpression, _visitChild(node.calledExpression)) + .with(\.arguments, _visitChild(node.arguments)), + originalWas: node + ) + } + + 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))) - } - """ + if node.operator.tokenKind == .prefixOperator("!") { + // Do not break apart the boolean negation operator from its expression + // (it adds visual noise but is really just flipping a bit.) + return _rewrite(node) + } + + 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 == "!==" { + + let lhsName = context.makeUniqueClosureParameterName("lhs", in: effectiveRootNode) + let rhsName = context.makeUniqueClosureParameterName("rhs", in: effectiveRootNode) + return _rewrite( + ClosureExprSyntax( + signature: ClosureSignatureSyntax( + leadingTrivia: .space, + parameterClause: .simpleInput( + ClosureShorthandParameterListSyntax { + ClosureShorthandParameterSyntax(name: lhsName) + ClosureShorthandParameterSyntax(name: rhsName) + } + ), + returnClause: ReturnClauseSyntax( + leadingTrivia: .space, + type: MemberTypeSyntax( + leadingTrivia: .space, + baseType: IdentifierTypeSyntax(name: .identifier("Swift")), + name: .identifier("Bool") + ), + trailingTrivia: .space + ), + inKeyword: .keyword(.in), + trailingTrivia: .space + ) + ) { + InfixOperatorExprSyntax( + leftOperand: DeclReferenceExprSyntax(baseName: lhsName, trailingTrivia: .space), + operator: BinaryOperatorExprSyntax(text: op), + rightOperand: DeclReferenceExprSyntax(leadingTrivia: .space, baseName: rhsName, trailingTrivia: .space) + ) + }, + originalWas: node, + calling: .identifier("__cmp"), + passing: [ + Argument(expression: _visitChild(node.leftOperand)), + Argument(expression: node.leftOperand.expressionID(rootedAt: effectiveRootNode, in: context)), + Argument(expression: _visitChild(node.rightOperand)), + Argument(expression: node.rightOperand.expressionID(rootedAt: effectiveRootNode, in: context)) + ] ) + } + + 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, calling: .identifier("__inoutAfter")) + if rewrittenExpr != ExprSyntax(node) { + 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: - Variadics + + override func visit(_ node: PackExpansionExprSyntax) -> ExprSyntax { + // We cannot expand parameter pack expressions. + isCancelled = true + return ExprSyntax(node) + } + + override func visit(_ node: PackElementExprSyntax) -> ExprSyntax { + // We cannot expand parameter pack expressions. + isCancelled = true + 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, in: context)) + ] + ) } - 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: 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) + } } - // 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) + override func visit(_ node: IsExprSyntax) -> ExprSyntax { + _rewriteAsCast(node.expression, .is, node.type, originalWas: 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) + // 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 negation. - if let negatedExpr = _negatedExpression(expr) { - return _parseCondition(negating: negatedExpr.0, isParenthetical: negatedExpr.isParenthetical, 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) } - // 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: FloatLiteralExprSyntax) -> ExprSyntax { + _rewrite(node) } - return Condition(expression: expr) -} + override func visit(_ node: StringLiteralExprSyntax) -> ExprSyntax { + _rewrite(node) + } -// MARK: - + 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 { - /// Parse a condition argument from an arbitrary expression. + /// Rewrite and expand upon an expression node. /// /// - Parameters: - /// - expr: The condition expression to parse. - /// - macro: The macro expression being expanded. + /// - 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. + /// - expectationContextName: 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: An instance of ``Condition`` describing `expr`. - static func parseCondition(from expr: ExprSyntax, for macro: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> Condition { - // If the condition involves the `unsafe`, `try`, or `await` keywords, assume - // we cannot expand it. - let effectKeywordsFromNode = findEffectKeywords(in: expr) - guard effectKeywordsFromNode.intersection([.unsafe, .try, .await]).isEmpty else { - return Condition(expression: expr) + /// - Returns: The expanded form of `node` as a closure expression that calls + /// the expression context named `expectationContextName` as well as the set + /// of rewritten subnodes in `node`, or `nil` if the expression could not be + /// rewritten. + static func rewrite( + _ node: some ExprSyntaxProtocol, + usingExpectationContextNamed expectationContextName: 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), expectationContextName: expectationContextName) + var expandedExpr = contextInserter.rewrite(node, detach: true).cast(ExprSyntax.self) + if contextInserter.isCancelled { + return nil } - let effectKeywordsInLexicalContext = findEffectKeywords(in: context) - guard effectKeywordsInLexicalContext.intersection([.unsafe, .await]).isEmpty else { - return Condition(expression: expr) + let rewrittenNodes = contextInserter.rewrittenNodes + + // Insert additional effect keywords/thunks as needed. + var effectKeywordsToApply = effectKeywordsToApply + if isThrowing { + effectKeywordsToApply.insert(.try) } - if !isThrowing && effectKeywordsInLexicalContext.contains(.try) { - return Condition(expression: expr) + 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( + returnKeyword: .keyword(.return, trailingTrivia: .space), + expression: expandedExpr, + trailingTrivia: .space + ) + } } - _diagnoseTrivialBooleanValue(from: expr, for: macro, in: context) - let result = _parseCondition(from: expr, for: macro, in: context) - return result + // 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: expectationContextName, + colon: .colonToken(trailingTrivia: .space), + type: TypeSyntax( + MemberTypeSyntax( + baseType: IdentifierTypeSyntax(name: .identifier("Testing")), + name: .identifier("__ExpectationContext"), + genericArgumentClause: returnType.map { returnType in + GenericArgumentClauseSyntax { + GenericArgumentSyntax(argument: .type(TypeSyntax(returnType))) + } + } + ) + ) + ) + } + ) + ), + returnClause: returnType.map { returnType in + ReturnClauseSyntax( + arrow: .arrowToken(leadingTrivia: .space, trailingTrivia: .space), + type: returnType + ) + }, + inKeyword: .keyword(.in, leadingTrivia: .space, trailingTrivia: .space) + ), + 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: - Replacing dollar identifiers + +/// Rewrite a dollar identifier as a normal (non-dollar) identifier. +/// +/// - Parameters: +/// - token: The dollar identifier token to rewrite. +/// +/// - 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. +/// - context: The macro context in which the expression is being parsed. +/// +/// - 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, in context: some MacroExpansionContext) -> 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, in: context), + 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. +/// - context: The macro context in which the expression is being parsed. +/// +/// - 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, in context: some MacroExpansionContext) -> DictionaryExprSyntax { + createDictionaryExpr(forSourceCodeOf: CollectionOfOne(node), rootedAt: node, in: context) +} diff --git a/Sources/TestingMacros/Support/SourceCodeCapturing.swift b/Sources/TestingMacros/Support/SourceCodeCapturing.swift deleted file mode 100644 index 3e18e4713..000000000 --- a/Sources/TestingMacros/Support/SourceCodeCapturing.swift +++ /dev/null @@ -1,119 +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 SwiftParser -import SwiftSyntax -import SwiftSyntaxBuilder - -/// 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/TestingMacrosMain.swift b/Sources/TestingMacros/TestingMacrosMain.swift index 074aeb86b..e62161006 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 2d6229ed5..2d64df7e9 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; +} + #if defined(_WIN32) static inline LPCSTR swt_IDI_SHIELD(void) { return IDI_SHIELD; diff --git a/Tests/SubexpressionShowcase/SubexpressionShowcase.swift b/Tests/SubexpressionShowcase/SubexpressionShowcase.swift new file mode 100644 index 000000000..223c99175 --- /dev/null +++ b/Tests/SubexpressionShowcase/SubexpressionShowcase.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 +// + +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 { + let fff = false + let ttt = true + #expect(false || true) + + #expect((fff == ttt) == ttt) + + #expect((Int)(123) == 124) + #expect((Int, Double)(123, 456.0) == (124, 457.0)) + #expect((123, 456) == (789, 0x12)) + #expect((try g() > 500) && true) + + #expect(!Bool(true)) + +#if false + // Unsupported: mutating member functions require inout semantics we can't + // provide (since we can't see where we might need to insert an `&` sigil). + // However, we _can_ provide a reasonable diagnostic for it now! + do { + struct S { + mutating func f() -> Bool { false } + } + var s = S() + #expect(s.f()) + } +#endif + + 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()) +#if false + #expect(s.j()) // consuming -- this DOES still fail, no syntax-level way to tell +#endif +#endif + + do { + struct E: ~Escapable { + borrowing func h() -> Bool { false } + consuming func j() -> Bool { false } + } + + let e = E() + #expect(e.h()) + #expect(e.j()) + } + + 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)) + + func k2(_ x: @escaping @autoclosure () -> Bool) async -> Bool { + x() + } + #expect(await k2(true)) + + class NonSendableClass { + var string: String = "" + } + func k3(_ x: NonSendableClass) async -> Bool { + (x as NonSendableClass?) == nil + } + let nonSendableObject = NonSendableClass() + #expect(await k3(nonSendableObject)) + extendLifetime(nonSendableObject) + +#if false + // Unsupported: __ec necessarily captures non-sendable state, so this will + // fail to compile because it is capturing __ec in a sendable closure. We + // could add locks guarding __ec's mutable state and eagerly capture state, + // but that would slow down tests significantly. The type checker cannot + // handle the number of `where T: Sendable` overloads of various functions + // that we would need in order to provide eager capture only for non-sendable + // values. However, this is a relatively narrow case, so for now we'll just + // accept it as unsupported and tell affected test authors to refactor their + // expectations so as to call m(_:) _before_ #expect(). + 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 dc864347c..446dedda9 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: 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: 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: 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: 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: Testing.__ExpectationContext) -> Swift.Bool in __ec(__ec((__ec(__ec((__ec(__ec(true, 0xf7a) || __ec(false, 0x877a), 0x77a)), 0x7a) && __ec(true, 0x10003a), 0x3a)), 0x2) || __ec(Bool.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()"], 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: 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: Testing.__ExpectationContext) -> Swift.Bool in __ec.__cmp({ lhs, rhs -> Swift.Bool in lhs == rhs }, 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: Testing.__ExpectationContext) -> Swift.Bool in __ec(!Bool.random(), 0x0) }, sourceCode: [0x0: "!Bool.random()"], 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: 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: 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: 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: 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: 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: 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: 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: 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: Testing.__ExpectationContext) -> Swift.Bool in defer { __ec.__inoutAfter(&c, 0x700) } return __ec(__ec(a.self, 0x6).b(&c), 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.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in defer { __ec.__inoutAfter(&c, 0x700) __ec.__inoutAfter(&d.e, 0x18100) } return __ec(__ec(a.self, 0x6).b(&c, &d.e), 0x0) }, sourceCode: [0x0: "a.b(&c, &d.e)", 0x6: "a", 0x700: "&c", 0x18100: "&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: Testing.__ExpectationContext) -> Swift.Bool in defer { __ec.__inoutAfter(&c, 0x700) } return __ec(__ec(a.self, 0x6).b(&c, __ec(d, 0x18100)), 0x0) }, sourceCode: [0x0: "a.b(&c, d)", 0x6: "a", 0x700: "&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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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()"##, - ##"#require(false)"##: - ##"Testing.__checkValue(false, expression: .__fromSyntaxNode("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()"##, - ##"#require(2 > 1)"##: - ##"Testing.__checkBinaryOperation(2, { $0 > $1() }, 1, expression: .__fromBinaryOperation(.__fromSyntaxNode("2"), ">", .__fromSyntaxNode("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()"##, - ##"#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()"##, - ##"#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()"##, - ##"#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()"##, - ##"#require((true && false))"##: - ##"Testing.__checkBinaryOperation(true, { $0 && $1() }, false, expression: .__fromBinaryOperation(.__fromSyntaxNode("true"), "&&", .__fromSyntaxNode("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()"##, - ##"#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()"##, - ##"#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()"##, - ##"#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()"##, - ##"#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()"##, - ##"#require(a, "b", c: c)"##: - ##"Testing.__checkValue(a, c: c, expression: .__fromSyntaxNode("a"), 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()"##, - ##"#require(b(c))"##: - ##"Testing.__checkFunctionCall((), calling: { b($1) }, c, expression: .__fromFunctionCall(nil, "b", (nil, .__fromSyntaxNode("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()"##, - ##"#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()"##, - ##"#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()"##, - ##"#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()"##, - ##"#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()"##, - ##"#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()"##, - ##"#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()"##, - ##"#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()"##, - ##"#require({}())"##: - ##"Testing.__checkValue({}(), expression: .__fromSyntaxNode("{}()"), 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()"##, - ##"#require(a.b { c })"##: - ##"Testing.__checkValue(a.b { c }, expression: .__fromSyntaxNode("a.b { c }"), comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, - ##"#require(a, sourceLocation: someValue)"##: - ##"Testing.__checkValue(a, expression: .__fromSyntaxNode("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()"##, - ##"#require(a???.isB)"##: - ##"Testing.__checkPropertyAccess(a.self, getting: { $0???.isB }, expression: .__fromPropertyAccess(.__fromSyntaxNode("a"), .__fromSyntaxNode("isB")), 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()"##, - ##"#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()"##, - ##"#require(isolation: somewhere) {}"##: - ##"Testing.__checkClosureCall(performing: {}, expression: .__fromSyntaxNode("{}"), comments: [], isRequired: true, isolation: somewhere, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"try #require(true)"##: + ##"try Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(true, 0x0)) }, sourceCode: [0x0: "true"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"try #require(false)"##: + ##"try Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(false, 0x0)) }, sourceCode: [0x0: "false"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"try #require(false, "Custom message")"##: + ##"try Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(false, 0x0)) }, sourceCode: [0x0: "false"], comments: ["Custom message"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"try #require(2 > 1)"##: + ##"try Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(2 > 1, 0x0)) }, sourceCode: [0x0: "2 > 1"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"try #require(((true || false) && true) || Bool.random())"##: + ##"try Testing.__checkCondition({ (__ec: 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(Bool.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()"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"try #require(9 > 8 && 7 > 6, "Some comment")"##: + ##"try Testing.__checkCondition({ (__ec: 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()"##, + ##"try #require("a" == "b")"##: + ##"try Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec.__cmp({ lhs, rhs -> Swift.Bool in lhs == rhs }, 0x0, "a", 0x2, "b", 0x200)) }, sourceCode: [0x0: #""a" == "b""#], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"try #require(!Bool.random())"##: + ##"try Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(!Bool.random(), 0x0)) }, sourceCode: [0x0: "!Bool.random()"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"try #require((true && false))"##: + ##"try Testing.__checkCondition({ (__ec: 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()"##, + ##"try #require(try x())"##: + ##"try Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in try __ec(x(), 0x4) }, sourceCode: [0x4: "x()"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"try #require(1 is Int)"##: + ##"try Testing.__checkCondition({ (__ec: 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()"##, + ##"try #require("123") { 1 == 2 } then: { foo() }"##: + ##"try Testing.__checkClosureCall(performing: { 1 == 2 }, then: { foo() }, sourceCode: [0x0: "1 == 2"], comments: ["123"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"try #require("123") { let x = 0 }"##: + ##"try Testing.__checkClosureCall(performing: { let x = 0 }, sourceCode: [0x0: "let x = 0"], comments: ["123"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"try #require("123") { let x = 0; return x == 0 }"##: + ##"try 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()"##, + ##"try #require(a, "b", c: c)"##: + ##"try Testing.__checkCondition({ (__ec: 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()"##, + ##"try #require(a())"##: + ##"try Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(a(), 0x0)) }, sourceCode: [0x0: "a()"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"try #require(b(c))"##: + ##"try Testing.__checkCondition({ (__ec: 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()"##, + ##"try #require(a.b(c))"##: + ##"try Testing.__checkCondition({ (__ec: 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()"##, + ##"try #require(a.b(c, d: e))"##: + ##"try Testing.__checkCondition({ (__ec: 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()"##, + ##"try #require(a.b(&c))"##: + ##"try Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in defer { __ec.__inoutAfter(&c, 0x700) } return try Testing.__requiringTry(__ec(__ec(a.self, 0x6).b(&c), 0x0)) }, sourceCode: [0x0: "a.b(&c)", 0x6: "a", 0x700: "&c"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"try #require(a.b(&c, &d.e))"##: + ##"try Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in defer { __ec.__inoutAfter(&c, 0x700) __ec.__inoutAfter(&d.e, 0x18100) } return try Testing.__requiringTry(__ec(__ec(a.self, 0x6).b(&c, &d.e), 0x0)) }, sourceCode: [0x0: "a.b(&c, &d.e)", 0x6: "a", 0x700: "&c", 0x18100: "&d.e"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"try #require(a.b(&c, d))"##: + ##"try Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in defer { __ec.__inoutAfter(&c, 0x700) } return try Testing.__requiringTry(__ec(__ec(a.self, 0x6).b(&c, __ec(d, 0x18100)), 0x0)) }, sourceCode: [0x0: "a.b(&c, d)", 0x6: "a", 0x700: "&c", 0x18100: "d"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"try #require(a.b(try c()))"##: + ##"try Testing.__checkCondition({ (__ec: 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()"##, + ##"try #require(a?.b(c))"##: + ##"try Testing.__checkCondition({ (__ec: 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()"##, + ##"try #require(a???.b(c))"##: + ##"try Testing.__checkCondition({ (__ec: 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()"##, + ##"try #require(a?.b.c(d))"##: + ##"try Testing.__checkCondition({ (__ec: 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()"##, + ##"try #require({}())"##: + ##"try Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec({}(), 0x0)) }, sourceCode: [0x0: "{}()"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"try #require(a.b(c: d))"##: + ##"try Testing.__checkCondition({ (__ec: 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()"##, + ##"try #require(a.b { c })"##: + ##"try Testing.__checkCondition({ (__ec: 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()"##, + ##"try #require(a, sourceLocation: someValue)"##: + ##"try Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(a, 0x0)) }, sourceCode: [0x0: "a"], comments: [], isRequired: true, sourceLocation: someValue).__required()"##, + ##"try #require(a.isB)"##: + ##"try Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(a.isB, 0x0)) }, sourceCode: [0x0: "a.isB"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"try #require(a???.isB)"##: + ##"try Testing.__checkCondition({ (__ec: 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()"##, + ##"try #require(a?.b.isB)"##: + ##"try Testing.__checkCondition({ (__ec: 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()"##, + ##"try #require(a?.b().isB)"##: + ##"try Testing.__checkCondition({ (__ec: 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()"##, + ##"try #require(isolation: somewhere) {}"##: + ##"try 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()"##, + ##"try #requireUnwrap(Optional.none)"##: + ##"try Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Optional in try Testing.__requiringTry(__ec(Optional.none, 0x0)) }, sourceCode: [0x0: "Optional.none"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"try #requireUnwrap(nil ?? 123)"##: + ##"try Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Optional in try Testing.__requiringTry(__ec(nil ?? 123, 0x0)) }, sourceCode: [0x0: "nil ?? 123"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"try #requireUnwrap(123 ?? nil)"##: + ##"try Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Optional in try Testing.__requiringTry(__ec(123 ?? nil, 0x0)) }, sourceCode: [0x0: "123 ?? nil"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"try #requireUnwrap(123 as? Double)"##: + ##"try Testing.__checkCondition({ (__ec: 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()"##, + ##"try #requireUnwrap(123 as Double)"##: + ##"try Testing.__checkEscapedCondition(123 as Double, sourceCode: [0x0: "123 as Double"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"try #requireUnwrap(123 as! Double)"##: + ##"try Testing.__checkCondition({ (__ec: 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, r, s, t, u, v, w, x, y, z, a, b, c, d, e, f, g))"##: + ##"__ec(g, Testing.__ExpressionID(130, 129, 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: 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: Testing.__ExpectationContext) -> Swift.Bool in try __ec(x(), 0x4) }, sourceCode: [0x4: "x()"], comments: [.__line("// Capture me")], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected() """, """ @@ -238,7 +251,7 @@ 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: Testing.__ExpectationContext) -> Swift.Bool in try __ec(x(), 0x4) }, sourceCode: [0x4: "x()"], comments: [.__line("// Capture me")], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected() """, """ @@ -247,7 +260,7 @@ struct ConditionMacroTests { """: """ // Capture me - try Testing.__checkValue(x, expression: .__fromSyntaxNode("x"), comments: [.__line("// Capture me")], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected() + try Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(x, 0x0)) }, sourceCode: [0x0: "x"], comments: [.__line("// Capture me")], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected() """, """ @@ -256,7 +269,8 @@ struct ConditionMacroTests { """: """ // Capture me - await Testing.__checkValue(x, expression: .__fromSyntaxNode("x"), comments: [.__line("// Capture me")], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected() + await Testing.__checkConditionAsync({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in await Testing.__requiringAwait(__ec(x, 0x0)) }, sourceCode: [0x0: "x"], comments: [.__line("// Capture me")], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected() + """, """ @@ -275,7 +289,7 @@ struct ConditionMacroTests { // Comment for await await // Comment for expect - Testing.__checkValue(x, expression: .__fromSyntaxNode("x"), comments: [.__line("// Comment for try"), .__line("// Comment for await"), .__line("// Comment for expect")], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected() + Testing.__checkConditionAsync({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in try await Testing.__requiringTry(Testing.__requiringAwait(__ec(x, 0x0))) }, sourceCode: [0x0: "x"], comments: [.__line("// Comment for try"), .__line("// Comment for await"), .__line("// Comment for expect")], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected() """, """ @@ -288,17 +302,16 @@ struct ConditionMacroTests { """ func example() { // Capture me - Testing.__checkFunctionCall((), calling: { _ in - x() - }, expression: .__fromFunctionCall(nil, "x"), comments: [.__line("// Capture me")], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected() + Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in __ec(x(), 0x0) }, sourceCode: [0x0: "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", @@ -329,6 +342,18 @@ struct ConditionMacroTests { #expect(diagnostics.count == 0) } + @Test("#expect() expansion escape hatches", + arguments: [ + "#expect(x.y() as Bool)", + "#expect((x.y()) as Bool)", + "#expect((x.y() as Bool))", + ] + ) + func escapeHatches(input: String) throws { + let (actualOutput, _) = try parse(input) + #expect(actualOutput.contains("Testing.__checkEscapedCondition")) + } + @Test("as! warns when used with #require()", arguments: ["#expect(x as! T)", "#require(x as! T)",] ) @@ -495,25 +520,4 @@ struct ConditionMacroTests { #expect(diagnostic.message == expectedMessage) } } - - @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 453a631b2..ac5e44c7e 100644 --- a/Tests/TestingMacrosTests/TestSupport/Parse.swift +++ b/Tests/TestingMacrosTests/TestSupport/Parse.swift @@ -22,6 +22,7 @@ import SwiftSyntaxMacroExpansion fileprivate let allMacros: [String: any (Macro & Sendable).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/TestingMacrosTests/UniqueIdentifierTests.swift b/Tests/TestingMacrosTests/UniqueIdentifierTests.swift index 267c93aa9..511178c93 100644 --- a/Tests/TestingMacrosTests/UniqueIdentifierTests.swift +++ b/Tests/TestingMacrosTests/UniqueIdentifierTests.swift @@ -81,4 +81,21 @@ struct UniqueIdentifierTests { let uniqueName2 = try makeUniqueName("func f() { def() }") #expect(uniqueName1 == uniqueName2) } + + @Test("Synthesized closure arguments are uniqued", + arguments: [ + ##"#expect(abc)"##: + ##"Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in __ec(abc, 0x0) }, sourceCode: [0x0: "abc"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"#expect(__ec)"##: + ##"Testing.__checkCondition({ (__ec0: Testing.__ExpectationContext) -> Swift.Bool in __ec0(__ec, 0x0) }, sourceCode: [0x0: "__ec"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"#expect(__ec + __ec0)"##: + ##"Testing.__checkCondition({ (__ec1: Testing.__ExpectationContext) -> Swift.Bool in __ec1(__ec1(__ec, 0x2) + __ec1(__ec0, 0x20), 0x0) }, sourceCode: [0x0: "__ec + __ec0", 0x2: "__ec", 0x20: "__ec0"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ] + ) + func synthesizedClosureArgumentsUniqued(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(expectedOutput == actualOutput, "\(actualActual)") + } } diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 854ecd133..67bda09c1 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -957,7 +957,14 @@ struct MySendableAttachable: Attachable, Sendable { var string: String func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { +#if !SWT_FIXED_132334935 + let attachment = copy attachment +#endif +#if SWT_FIXED_163656720 #expect(attachment.attachableValue.string == string) +#else + #expect(attachment.attachableValue.string == self.string) +#endif 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 d489b2f1e..437ede15e 100644 --- a/Tests/TestingTests/EventRecorderTests.swift +++ b/Tests/TestingTests/EventRecorderTests.swift @@ -109,8 +109,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()')"#)) do { @@ -191,7 +191,7 @@ struct EventRecorderTests { .first != nil ) } - + @available(_regexAPI, *) @Test( "Log the total number of test cases in parameterized tests at the end of the test run", @@ -583,8 +583,10 @@ struct EventRecorderTests { // The first message is something along the lines of "Test foo recorded a // known issue" and includes a source location, so is inconvenient to // include in our expectation here. - let actualComments = messages.rawValue.dropFirst().map(\.stringValue) - #expect(actualComments == expectedComments) + let actualComments = messages.rawValue.map(\.stringValue) + for expectedComment in expectedComments { + #expect(actualComments.contains(expectedComment)) + } } } diff --git a/Tests/TestingTests/EventTests.swift b/Tests/TestingTests/EventTests.swift index 653ad2a87..c9b68f86a 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 7434e906f..22ff83910 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() @@ -1177,15 +1189,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") @@ -1201,7 +1212,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 { @@ -1219,7 +1251,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 { @@ -1282,33 +1332,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 @@ -1321,7 +1344,7 @@ final class IssueTests: XCTestCase { return } let expression = expectation.evaluatedExpression - XCTAssertTrue(expression.expandedDescription().contains("")) + assert(expression, contains: "") } @Sendable func rhs() -> Bool { @@ -1380,9 +1403,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)") } } @@ -1404,8 +1426,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") } } @@ -1458,8 +1479,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!") } } @@ -1520,9 +1540,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") } } @@ -1547,9 +1566,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") } } @@ -1666,7 +1684,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 9ce84e3fd..bd981909a 100644 --- a/Tests/TestingTests/MiscellaneousTests.swift +++ b/Tests/TestingTests/MiscellaneousTests.swift @@ -272,6 +272,11 @@ struct MultiLineSuite { _ = try #require(x?[...].last) } +@Test(.hidden) func canHaveVariableNamed__ec() throws { + let __ec = 1 + #expect(__ec == 1) +} + extension Bool { func throwingValue() throws -> Bool { self } } @@ -612,3 +617,23 @@ struct MiscellaneousTests { #expect(duration < .seconds(1)) } } + +#if compiler(>=6.2) +extension Span { + var isNonEmpty: Bool { !isEmpty } + var nonZeroCount: Int? { isEmpty ? nil : count } +} + +extension MiscellaneousTests { + @available(macOS 26, iOS 26, watchOS 26, tvOS 26, visionOS 26, *) + @Test("Instance property of nonescapable type") + func propertyAccessOfNonescapable() throws { + let array = [1, 2, 3, 4, 5] + let span = array.span + #expect(!span.isEmpty) + #expect(span.isNonEmpty) + let count = try #require(span.nonZeroCount) + #expect(count == span.count) + } +} +#endif diff --git a/Tests/TestingTests/Traits/TimeLimitTraitTests.swift b/Tests/TestingTests/Traits/TimeLimitTraitTests.swift index 7d427f259..d156dd544 100644 --- a/Tests/TestingTests/Traits/TimeLimitTraitTests.swift +++ b/Tests/TestingTests/Traits/TimeLimitTraitTests.swift @@ -210,13 +210,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/TypeInfoTests.swift b/Tests/TestingTests/TypeInfoTests.swift index 06c257a42..541597d01 100644 --- a/Tests/TestingTests/TypeInfoTests.swift +++ b/Tests/TestingTests/TypeInfoTests.swift @@ -125,7 +125,8 @@ struct TypeInfoTests { @Test func typeOfMoveOnlyValueIsInferred() { let value = MoveOnlyType() - #expect(TypeInfo(describingTypeOf: value).unqualifiedName == "MoveOnlyType") + let unqualifiedName = TypeInfo(describingTypeOf: value).unqualifiedName + #expect(unqualifiedName == "MoveOnlyType") } } 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)) }