From 99e080b454124a6e001f93ad5b99edd9d82b7f1e 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 0cb2fedf0..89dc2baf0 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftType.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftType.swift @@ -521,7 +521,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 2fc1abd25..e734fe176 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 typedThrowsOnInitializers() throws { let result = try analyze(