From d9a87711b203d2dfd58796b8eb9a44480bdbe9c4 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 21 Aug 2026 10:08:50 +0000 Subject: [PATCH] [SwiftExtract] Construct .existential for 'any P' types The someOrAnyType translation had identical branches: both 'some P' and 'any P' produced SwiftType.opaque, so .existential was never constructed and 'any P' rendered as 'some P'. Construct .existential for the 'any' spelling. JExtractSwiftLib already handles .opaque and .existential in shared case arms, so its behavior is unchanged. --- .../SwiftExtract/SwiftTypes/SwiftType.swift | 2 +- .../AnalysisResultTests.swift | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftType.swift b/Sources/SwiftExtract/SwiftTypes/SwiftType.swift index 34f358353..6a9a89141 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftType.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftType.swift @@ -525,7 +525,7 @@ extension SwiftType { if someOrAntType.someOrAnySpecifier.tokenKind == .keyword(.some) { self = .opaque(try SwiftType(someOrAntType.constraint, lookupContext: lookupContext)) } else { - self = .opaque(try SwiftType(someOrAntType.constraint, lookupContext: lookupContext)) + self = .existential(try SwiftType(someOrAntType.constraint, lookupContext: lookupContext)) } case .compositionType(let compositeType): diff --git a/Tests/SwiftExtractTests/AnalysisResultTests.swift b/Tests/SwiftExtractTests/AnalysisResultTests.swift index c942a7a54..901fe181e 100644 --- a/Tests/SwiftExtractTests/AnalysisResultTests.swift +++ b/Tests/SwiftExtractTests/AnalysisResultTests.swift @@ -393,6 +393,39 @@ struct AnalysisResultSuite { #expect(unresolvable.functionSignature.thrownTypedError == nil) } + @Test + func someAndAnyProduceOpaqueAndExistentialTypes() throws { + let result = try analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public protocol Swimmer {} + public func trainOpaque(_ swimmer: some Swimmer) {} + public func trainExistential(_ swimmer: any Swimmer) {} + """ + ) + ], + moduleName: "Aquarium" + ) + + let byName = Dictionary(uniqueKeysWithValues: result.extractedGlobalFuncs.map { ($0.name, $0) }) + + let opaque = try #require(byName["trainOpaque"]?.functionSignature.parameters.first?.type) + guard case .opaque = opaque else { + Issue.record("expected .opaque, got \(opaque)") + return + } + #expect(opaque.description == "some Swimmer") + + let existential = try #require(byName["trainExistential"]?.functionSignature.parameters.first?.type) + guard case .existential = existential else { + Issue.record("expected .existential, got \(existential)") + return + } + #expect(existential.description == "any Swimmer") + } + @Test func opaquePointerIsAKnownSwiftType() throws { let result = try analyze(