diff --git a/Package.swift b/Package.swift index 4360aabdc..74557c8e7 100644 --- a/Package.swift +++ b/Package.swift @@ -116,7 +116,8 @@ let package = Package( ], exclude: ["CMakeLists.txt", "Testing.swiftcrossimport"], cxxSettings: .packageSettings, - swiftSettings: .packageSettings + .enableLibraryEvolution(), + swiftSettings: .packageSettings + .enableLibraryEvolution() + + [.enableExperimentalFeature("LifetimeDependence")], linkerSettings: [ .linkedLibrary("execinfo", .when(platforms: [.custom("freebsd"), .openbsd])) ] @@ -130,6 +131,7 @@ let package = Package( "MemorySafeTestingTests", ], swiftSettings: .packageSettings + .disableMandatoryOptimizationsSettings + + [.enableExperimentalFeature("LifetimeDependence")] ), // Use a plain `.target` instead of a `.testTarget` to avoid the unnecessary diff --git a/Sources/Testing/Expectations/Expectation+Macro.swift b/Sources/Testing/Expectations/Expectation+Macro.swift index 973efde53..0dcadce0f 100644 --- a/Sources/Testing/Expectations/Expectation+Macro.swift +++ b/Sources/Testing/Expectations/Expectation+Macro.swift @@ -65,7 +65,7 @@ /// If `optionalValue` is `nil`, an ``Issue`` is recorded for the test that is /// running in the current task and an instance of ``ExpectationFailedError`` is /// thrown. -@freestanding(expression) public macro require( +@freestanding(expression) public macro require( _ optionalValue: T?, _ comment: @autoclosure () -> Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation @@ -123,7 +123,7 @@ public macro require( /// diagnostic indicating that the expectation is redundant. @freestanding(expression) @_documentation(visibility: private) -public macro require( +public macro require( _ optionalValue: T, _ comment: @autoclosure () -> Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation diff --git a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift index 6d3093f2a..de07bbe96 100644 --- a/Sources/Testing/Expectations/ExpectationChecking+Macro.swift +++ b/Sources/Testing/Expectations/ExpectationChecking+Macro.swift @@ -556,6 +556,36 @@ public func __checkPropertyAccess( ) } +/// Check that an expectation has passed after a condition has been evaluated +/// and throw an error if it failed, for nonescapable types. +/// +/// 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( + condition, + expression: expression, + expressionWithCapturedRuntimeValues: + expression.capturingRuntimeValues("", condition), + 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. /// @@ -586,6 +616,39 @@ public func __checkPropertyAccess( ) } +/// Check that an expectation has passed after a condition has been evaluated +/// and throw an error if it failed, for nonescapable values. +/// +/// This overload is used to conditionally unwrap optional values produced from +/// expanded property accesses: +/// +/// ```swift +/// let z = try #require(x.nearestFoodTruck) +/// ``` +/// +/// - Warning: This function is used to implement the `#expect()` and +/// `#require()` macros. Do not call it directly. +@lifetime(copy lhs) +public func __checkPropertyAccess( + _ lhs: T, + getting memberAccess: (T) -> U?, + expression: __Expression, + comments: @autoclosure () -> [Comment], + isRequired: Bool, + sourceLocation: SourceLocation +) -> Result { + let optionalValue = _overrideLifetime(memberAccess(lhs), copying: lhs) + return __checkValue( + optionalValue, + expression: expression, + expressionWithCapturedRuntimeValues: + expression.capturingRuntimeValues("", ""), + comments: comments(), + isRequired: isRequired, + sourceLocation: sourceLocation + ) +} + // MARK: - Collection diffing /// Check that an expectation has passed after a condition has been evaluated @@ -756,6 +819,46 @@ public func __checkValue( } } +/// Check that an expectation has passed after a condition has been evaluated +/// and throw an error if it failed, for a nonescapable type. +/// +/// 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. +@lifetime(copy optionalValue) +public func __checkValue( + _ optionalValue: T?, + expression: __Expression, + expressionWithCapturedRuntimeValues: @autoclosure () -> __Expression? = nil, + comments: @autoclosure () -> [Comment], + isRequired: Bool, + sourceLocation: SourceLocation +) -> Result { + let result = __checkValue( + optionalValue != nil, + expression: expression, + expressionWithCapturedRuntimeValues: + (expressionWithCapturedRuntimeValues() ?? expression) + .capturingRuntimeValue(optionalValue == nil ? "nil" : ""), + comments: comments(), + isRequired: isRequired, + sourceLocation: sourceLocation + ) + // Result.map doesn't support ~Escapable, so we do a manual conversion: + return switch result { + case .success: + .success(optionalValue.unsafelyUnwrapped) + case .failure(let error): + .failure(error) + } +} + /// Check that an expectation has passed after a condition has been evaluated /// and throw an error if it failed. /// diff --git a/Sources/Testing/Support/Additions/ResultAdditions.swift b/Sources/Testing/Support/Additions/ResultAdditions.swift index 9a2e6ea5a..3b2fbf37c 100644 --- a/Sources/Testing/Support/Additions/ResultAdditions.swift +++ b/Sources/Testing/Support/Additions/ResultAdditions.swift @@ -8,7 +8,7 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -extension Result { +extension Result where Success: ~Escapable { /// Handle this instance as if it were returned from a call to `#expect()`. /// /// - Warning: This function is used to implement the `#expect()` and @@ -19,6 +19,7 @@ extension Result { /// /// - Warning: This function is used to implement the `#expect()` and /// `#require()` macros. Do not call it directly. + @lifetime(copy self) @inlinable public func __required() throws -> Success { try get() } diff --git a/Tests/TestingTests/MiscellaneousTests.swift b/Tests/TestingTests/MiscellaneousTests.swift index b895f6c1b..ab05dd693 100644 --- a/Tests/TestingTests/MiscellaneousTests.swift +++ b/Tests/TestingTests/MiscellaneousTests.swift @@ -600,4 +600,50 @@ struct MiscellaneousTests { } #expect(duration < .seconds(1)) } + + @Test("Instance property of custom nonescapable type") + func nonEscapableValue() throws { + struct NonEscapableValue: ~Escapable { + private(set) var value: Int + + @lifetime(borrow value) + init(_ value: Int) { + self.value = value + } + + var isZero: Bool { value == 0 } + var isNonZero: Bool { value != 0 } + var nonZeroValue: Int? { value == 0 ? nil : value } + + var incremented: NonEscapableValue? { + @lifetime(copy self) + get { + var result = self + result.value += 1 + return result + } + } + } + + let int = 2 + let nev = NonEscapableValue(int) + #expect(!nev.isZero) + #expect(nev.isNonZero) + let v = try #require(nev.nonZeroValue) + #expect(v == int) + + // The following errors due to the emitted code looking like: + // + // Testing.__checkPropertyAccess(nev.self, { $0.incremented }, ...) + // + // Since the closure is just declared as `(T) -> U?`, without any lifetime + // annotation saying that the `U?` result copies the lifetime of the + // input parameter `T`, compilation results in: + // + // error: Lifetime-dependent value escapes its scope + // note: It depends on the lifetime of argument '$0' + // + // let nev2 = try #require(nev.incremented) + // #expect(nev2.isNonZero) + } }