diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ActorProperties.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ActorProperties.swift index d5ef8ce87..d63e8a46c 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ActorProperties.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ActorProperties.swift @@ -27,3 +27,15 @@ public actor IdentifiedActor: ActorIdentifier { self.unwitnessed = identifier + 1 } } + +public protocol SendableActorIdentifier: AnyObject, Sendable { + var identifier: Int64 { get } +} + +public actor SendableIdentifiedActor: SendableActorIdentifier { + public let identifier: Int64 + + public init(identifier: Int64) { + self.identifier = identifier + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ActorPropertiesTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ActorPropertiesTest.java index 37486af44..9d20c2dc2 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ActorPropertiesTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ActorPropertiesTest.java @@ -22,6 +22,15 @@ import static org.junit.jupiter.api.Assertions.*; public class ActorPropertiesTest { + @Test + void standardProtocolConstraintsKeepWitnessesSynchronous() { + try (var arena = SwiftArena.ofConfined()) { + SendableIdentifiedActor actor = SendableIdentifiedActor.init(42, arena); + long identifier = actor.getIdentifier(); + assertEquals(42, identifier); + } + } + @Test void nonisolatedPropertiesStaySynchronous() { try (var arena = SwiftArena.ofConfined()) { diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift index 1e63329c8..72af981c5 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift @@ -449,7 +449,15 @@ extension SwiftFunctionSignature { else { return false } if !visited.insert(declaration.identity).inserted { return true } return (declaration.inheritanceTypes ?? []).allSatisfy { inherited in - guard let type = try? SwiftType(inherited.type, lookupContext: lookupContext) else { return false } + let type = try? SwiftType(inherited.type, lookupContext: lookupContext) + // These standard constraints do not introduce actor isolation and are + // absent from SwiftExtract's built-in Swift symbol table. + if ["AnyObject", "Swift.AnyObject", "Sendable", "Swift.Sendable"].contains(inherited.type.trimmedDescription), + type == nil || type?.asNominalTypeDeclaration?.moduleName == "Swift" + { + return true + } + guard let type else { return false } return isNonisolatedProtocol(type, visited: &visited) } } diff --git a/Tests/JExtractSwiftTests/JNI/JNIActorPropertyTests.swift b/Tests/JExtractSwiftTests/JNI/JNIActorPropertyTests.swift index 2cdc603e8..51b3f159b 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIActorPropertyTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIActorPropertyTests.swift @@ -64,4 +64,26 @@ struct JNIActorPropertyTests { ] ) } + + @Test(arguments: ["AnyObject", "Swift.AnyObject", "Sendable", "Swift.Sendable", "AnyObject, Swift.Sendable"]) + func standardConstraintsPreserveSynchronousBindings(_ inheritance: String) throws { + let source = """ + public protocol Identified: \(inheritance) { var identifier: Int64 { get } } + public actor Record: Identified { public let identifier: Int64 } + """ + try assertOutput( + input: source, + .jni, + .java, + expectedChunks: ["public long getIdentifier() {"], + notExpectedChunks: ["public java.util.concurrent.CompletableFuture getIdentifier() {"] + ) + try assertOutput( + input: source, + .jni, + .swift, + expectedChunks: ["return (selfPointer$.pointee as any Identified).identifier.getJNILocalRefValue(in: environment)"], + notExpectedChunks: ["await selfPointer$.pointee.identifier"] + ) + } } diff --git a/Tests/SwiftExtractTests/ActorPropertyIsolationTests.swift b/Tests/SwiftExtractTests/ActorPropertyIsolationTests.swift index fa4056f26..58fcb0840 100644 --- a/Tests/SwiftExtractTests/ActorPropertyIsolationTests.swift +++ b/Tests/SwiftExtractTests/ActorPropertyIsolationTests.swift @@ -65,6 +65,47 @@ struct ActorPropertyIsolationTests { } } + @Test(arguments: ["AnyObject", "Swift.AnyObject", "Sendable", "Swift.Sendable", "AnyObject, Swift.Sendable"]) + func standardConstraintsPreserveSynchronousWitness(_ inheritance: String) throws { + let source = """ + public protocol Identified: \(inheritance) { var identifier: Int64 { get } } + public actor Record: Identified { public let identifier: Int64 } + """ + let result = try analyze(sources: [("/fake/Source.swift", source)], moduleName: "Test") + let record = try #require(result.extractedTypes.values.first { $0.swiftNominal.name == "Record" }) + let accessor = try #require(record.variables.first { $0.name == "identifier" }) + #expect(accessor.functionSignature.nonisolatedWitnessProtocol?.asNominalTypeDeclaration?.name == "Identified") + #expect(!accessor.functionSignature.isImplicitlyAsync) + } + + @Test + func inheritedStandardConstraintsPreserveSynchronousWitness() throws { + let source = """ + public protocol Identified: AnyObject, Sendable { var identifier: Int64 { get } } + public protocol InheritedIdentifier: Identified {} + public actor Record: InheritedIdentifier { public let identifier: Int64 } + """ + let result = try analyze(sources: [("/fake/Source.swift", source)], moduleName: "Test") + let record = try #require(result.extractedTypes.values.first { $0.swiftNominal.name == "Record" }) + let accessor = try #require(record.variables.first { $0.name == "identifier" }) + #expect(accessor.functionSignature.nonisolatedWitnessProtocol?.asNominalTypeDeclaration?.name == "InheritedIdentifier") + #expect(!accessor.functionSignature.isImplicitlyAsync) + } + + @Test + func localSendableDoesNotBypassIsolation() throws { + let source = """ + @MainActor public protocol Sendable {} + public protocol Identified: Sendable { var identifier: Int64 { get } } + public actor Record: Identified { public let identifier: Int64 } + """ + let result = try analyze(sources: [("/fake/Source.swift", source)], moduleName: "Test") + let record = try #require(result.extractedTypes.values.first { $0.swiftNominal.name == "Record" }) + let accessor = try #require(record.variables.first { $0.name == "identifier" }) + #expect(accessor.functionSignature.nonisolatedWitnessProtocol == nil) + #expect(accessor.functionSignature.isImplicitlyAsync) + } + private func record() throws -> ExtractedNominalType { let result = try analyze(sources: [("/fake/Source.swift", Self.source)], moduleName: "Test") return try #require(result.extractedTypes.values.first { $0.swiftNominal.name == "Record" })