diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Typealias.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Typealias.swift new file mode 100644 index 000000000..d0b0f7a9b --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Typealias.swift @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +public typealias Amount = Double + +public struct TypealiasUser { + public var amount: Amount + + public init(amount: Amount) { + self.amount = amount + } + + public func doubled() -> Amount { + amount * 2 + } +} + +public func makeAmount(_ value: Amount) -> Amount { + value +} + +// Generic typealias used with a use-site argument. The alias's generic +// parameter `T` is substituted at the use site so `Maybe` resolves +// to `Optional`, which is `java.lang.OptionalLong` in Java. +public typealias Maybe = T? + +public func unwrapOrZero(_ value: Maybe) -> Int64 { + value ?? 0 +} diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/TypealiasUserTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/TypealiasUserTest.java new file mode 100644 index 000000000..3bc14ebe4 --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/TypealiasUserTest.java @@ -0,0 +1,49 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.ffm.AllocatingSwiftArena; + +import java.util.OptionalLong; + +import static org.junit.jupiter.api.Assertions.*; + +public class TypealiasUserTest { + @Test + void plainTypealiasResolvesStructMembers() { + try (var arena = AllocatingSwiftArena.ofConfined()) { + var user = TypealiasUser.init(2.5, arena); + assertEquals(2.5, user.getAmount(), 0.0); + assertEquals(5.0, user.doubled(), 0.0); + + user.setAmount(7.0); + assertEquals(7.0, user.getAmount(), 0.0); + } + } + + @Test + void freeFunctionThroughAliasIsExported() { + assertEquals(42.0, MySwiftLibrary.makeAmount(42.0), 0.0); + } + + @Test + void genericTypeAliasSubstitutesUseSiteArguments() { + // `Maybe` substitutes T -> Int64, resolving to Optional, + // which is then mapped to java.util.OptionalLong. + assertEquals(0L, MySwiftLibrary.unwrapOrZero(OptionalLong.empty())); + assertEquals(123L, MySwiftLibrary.unwrapOrZero(OptionalLong.of(123L))); + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Typealias.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Typealias.swift new file mode 100644 index 000000000..d0b0f7a9b --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Typealias.swift @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +public typealias Amount = Double + +public struct TypealiasUser { + public var amount: Amount + + public init(amount: Amount) { + self.amount = amount + } + + public func doubled() -> Amount { + amount * 2 + } +} + +public func makeAmount(_ value: Amount) -> Amount { + value +} + +// Generic typealias used with a use-site argument. The alias's generic +// parameter `T` is substituted at the use site so `Maybe` resolves +// to `Optional`, which is `java.lang.OptionalLong` in Java. +public typealias Maybe = T? + +public func unwrapOrZero(_ value: Maybe) -> Int64 { + value ?? 0 +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/TypealiasUserTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/TypealiasUserTest.java new file mode 100644 index 000000000..220de4743 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/TypealiasUserTest.java @@ -0,0 +1,49 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.SwiftArena; + +import java.util.OptionalLong; + +import static org.junit.jupiter.api.Assertions.*; + +public class TypealiasUserTest { + @Test + void plainTypealiasResolvesStructMembers() { + try (var arena = SwiftArena.ofConfined()) { + var user = TypealiasUser.init(2.5, arena); + assertEquals(2.5, user.getAmount(), 0.0); + assertEquals(5.0, user.doubled(), 0.0); + + user.setAmount(7.0); + assertEquals(7.0, user.getAmount(), 0.0); + } + } + + @Test + void freeFunctionThroughAliasIsExported() { + assertEquals(42.0, MySwiftLibrary.makeAmount(42.0), 0.0); + } + + @Test + void genericTypeAliasSubstitutesUseSiteArguments() { + // `Maybe` substitutes T -> Int64, resolving to Optional, + // which is then mapped to java.util.OptionalLong. + assertEquals(0L, MySwiftLibrary.unwrapOrZero(OptionalLong.empty())); + assertEquals(123L, MySwiftLibrary.unwrapOrZero(OptionalLong.of(123L))); + } +} diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftModuleSymbolTable.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftModuleSymbolTable.swift index 2db19928e..f42d750fb 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftModuleSymbolTable.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftModuleSymbolTable.swift @@ -28,6 +28,9 @@ struct SwiftModuleSymbolTable: SwiftSymbolTableProtocol { /// The top-level nominal types, found by name. var topLevelTypes: [String: SwiftNominalTypeDeclaration] = [:] + /// The top-level typealias declarations, found by name. + var topLevelTypeAliases: [String: SwiftTypeAliasDeclaration] = [:] + /// The nested types defined within this module. The map itself is indexed by the /// identifier of the nominal type declaration, and each entry is a map from the nested /// type name to the nominal type declaration. @@ -38,6 +41,11 @@ struct SwiftModuleSymbolTable: SwiftSymbolTableProtocol { topLevelTypes[name] } + /// Look for a top-level typealias with the given name. + func lookupTopLevelTypealias(_ name: String) -> SwiftTypeAliasDeclaration? { + topLevelTypeAliases[name] + } + // Look for a nested type with the given name. func lookupNestedType(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftNominalTypeDeclaration? { nestedTypes[parent]?[name] diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift index ba70a578c..21edbfd96 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift @@ -203,6 +203,24 @@ package class SwiftGenericParameterDeclaration: SwiftTypeDeclaration { } } +/// A plain typealias will resolve as the right hand type in generated code. +/// +/// A typealias used as a specialization of a generic type will be emitted as +/// a new concrete type in the Java. This way we can specialize `FishBox` from +/// `Box` by doing `typealias FishBox = Box`. +package final class SwiftTypeAliasDeclaration: SwiftTypeDeclaration { + let syntax: TypeAliasDeclSyntax + + init( + sourceFilePath: String, + moduleName: String, + node: TypeAliasDeclSyntax + ) { + self.syntax = node + super.init(sourceFilePath: sourceFilePath, moduleName: moduleName, name: node.name.text) + } +} + extension SwiftTypeDeclaration: Equatable { package static func == (lhs: SwiftTypeDeclaration, rhs: SwiftTypeDeclaration) -> Bool { lhs === rhs diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift index f8e5927c9..f2dd1fb3e 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift @@ -81,9 +81,29 @@ extension SwiftParsedModuleSymbolTableBuilder { self.handle(extensionDecl: extensionNode, sourceFilePath: sourceFilePath) } else if let ifConfigNode = decl.as(IfConfigDeclSyntax.self) { self.handle(ifConfig: ifConfigNode, sourceFilePath: sourceFilePath) + } else if let typeAliasNode = decl.as(TypeAliasDeclSyntax.self) { + self.handle(typeAliasDecl: typeAliasNode, sourceFilePath: sourceFilePath) } } + mutating func handle( + typeAliasDecl node: TypeAliasDeclSyntax, + sourceFilePath: String + ) { + let name = node.name.text + if symbolTable.topLevelTypeAliases[name] != nil + || symbolTable.lookupTopLevelNominalType(name) != nil + { + log?.debug("Failed to add a typealias into symbol table: redeclaration; \(name)") + return + } + symbolTable.topLevelTypeAliases[name] = SwiftTypeAliasDeclaration( + sourceFilePath: sourceFilePath, + moduleName: moduleName, + node: node + ) + } + /// Add a nominal type declaration and all of the nested types within it to the symbol /// table. mutating func handle( diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift index 34e8c6a4f..2a3796344 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift @@ -28,6 +28,9 @@ package protocol SwiftSymbolTableProtocol { // Look for a nested type with the given name. func lookupNestedType(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftNominalTypeDeclaration? + + /// Look for a top-level typealias with the given name. + func lookupTopLevelTypealias(_ name: String) -> SwiftTypeAliasDeclaration? } extension SwiftSymbolTableProtocol { @@ -178,6 +181,21 @@ extension SwiftSymbolTable: SwiftSymbolTableProtocol { return nil } + + /// Look for a top-level typealias with the given name. + package func lookupTopLevelTypealias(_ name: String) -> SwiftTypeAliasDeclaration? { + if let parsedResult = parsedModule.lookupTopLevelTypealias(name) { + return parsedResult + } + + for importedModule in prioritySortedImportedModules { + if let result = importedModule.lookupTopLevelTypealias(name) { + return result + } + } + + return nil + } } extension SwiftSymbolTable { diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift index e0752ea6d..0c9901114 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift @@ -466,6 +466,29 @@ extension SwiftType { ) } else if let genericParamDecl = typeDecl as? SwiftGenericParameterDeclaration { self = .genericParameter(genericParamDecl) + } else if let aliasDecl = typeDecl as? SwiftTypeAliasDeclaration { + let aliasGenericParams = + aliasDecl.syntax.genericParameterClause?.parameters.map { $0.name.text } ?? [] + let useSiteArgs = genericArguments ?? [] + + // The alias's generic parameter count must match the use-site argument + // count. Treat any mismatch (including use-site args on a non-generic + // alias, or missing args on a generic alias) as unimplemented to fall + // through to silent drop. + guard aliasGenericParams.count == useSiteArgs.count else { + throw TypeTranslationError.unimplementedType(originalType) + } + + let resolved = try lookupContext.resolve(typeAlias: aliasDecl) + + if aliasGenericParams.isEmpty { + self = resolved + } else { + let substitutions = Dictionary( + uniqueKeysWithValues: zip(aliasGenericParams, useSiteArgs) + ) + self = resolved.substituting(genericParameters: substitutions) + } } else { fatalError("unknown SwiftTypeDeclaration: \(type(of: typeDecl))") } @@ -494,6 +517,55 @@ extension SwiftType { ) } + /// Substitute generic parameters *by name*. + /// + /// This is used e.g. by typealiases like `typealias Ano = Array`, + /// so usages like `Ano` become `Array`. + func substituting(genericParameters substitutions: [String: SwiftType]) -> SwiftType { + guard !substitutions.isEmpty else { return self } + + switch self { + case .nominal(let nominal): + return .nominal( + SwiftNominalType( + parent: nominal.parent, + sugarName: nominal.sugarName, + nominalTypeDecl: nominal.nominalTypeDecl, + genericArguments: nominal.genericArguments?.map { + $0.substituting(genericParameters: substitutions) + } + ) + ) + case .genericParameter(let decl): + return substitutions[decl.name] ?? self + case .function(var fn): + fn.parameters = fn.parameters.map { p in + var p = p + p.type = p.type.substituting(genericParameters: substitutions) + return p + } + fn.resultType = fn.resultType.substituting(genericParameters: substitutions) + return .function(fn) + case .metatype(let inner): + return .metatype(inner.substituting(genericParameters: substitutions)) + case .tuple(let elements): + return .tuple( + elements.map { + SwiftTupleElement( + label: $0.label, + type: $0.type.substituting(genericParameters: substitutions) + ) + } + ) + case .existential(let inner): + return .existential(inner.substituting(genericParameters: substitutions)) + case .opaque(let inner): + return .opaque(inner.substituting(genericParameters: substitutions)) + case .composite(let types): + return .composite(types.map { $0.substituting(genericParameters: substitutions) }) + } + } + /// Produce an expression that creates the metatype for this type in /// Swift source code. var metatypeReferenceExprSyntax: ExprSyntax { diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift index 71aec972a..559a850c8 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift @@ -24,6 +24,10 @@ class SwiftTypeLookupContext { private var typeDecls: [Syntax.ID: SwiftTypeDeclaration] = [:] + /// Set of typealias syntax ids currently being resolved, to break + /// cycles like `typealias A = B; typealias B = A`. + private var resolvingAliases: Set = [] + init(symbolTable: SwiftSymbolTable) { self.symbolTable = symbolTable } @@ -52,8 +56,12 @@ class SwiftTypeLookupContext { } case .lookForMembers(let scopeNode): - if let nominalDecl = try typeDeclaration(for: scopeNode, sourceFilePath: "FIXME.swift") { // FIXME: no path here // implement some node -> file - if let found = symbolTable.lookupNestedType(name.name, parent: nominalDecl as! SwiftNominalTypeDeclaration) { + if let typeDecl = try typeDeclaration(for: scopeNode, sourceFilePath: "FIXME.swift") { // FIXME: no path here // implement some node -> file + guard let nominalDecl = typeDecl as? SwiftNominalTypeDeclaration else { + // Member lookup on a non-nominal (e.g. a typealias) is not supported here. + continue + } + if let found = symbolTable.lookupNestedType(name.name, parent: nominalDecl) { return found } } @@ -69,8 +77,12 @@ class SwiftTypeLookupContext { } } + // maybe it's a typealias, can we resolve it to a known type? + if let nominal = symbolTable.lookupTopLevelNominalType(name.name) { + return nominal + } // Fallback to global symbol table lookup. - return symbolTable.lookupTopLevelNominalType(name.name) + return symbolTable.lookupTopLevelTypealias(name.name) } /// Find the first type declaration in the `LookupName` results. @@ -136,8 +148,12 @@ class SwiftTypeLookupContext { } typeDecl = nominalDecl } - case .typeAliasDecl: - fatalError("typealias not implemented") + case .typeAliasDecl(let node): + typeDecl = SwiftTypeAliasDeclaration( + sourceFilePath: sourceFilePath, + moduleName: symbolTable.moduleName, + node: node + ) case .associatedTypeDecl: fatalError("associatedtype not implemented") default: @@ -185,6 +201,17 @@ class SwiftTypeLookupContext { } return nil } + + /// Resolve a typealias to the `SwiftType` of its right-hand side. + func resolve(typeAlias decl: SwiftTypeAliasDeclaration) throws -> SwiftType { + let id = decl.syntax.id + guard !resolvingAliases.contains(id) else { + throw TypeTranslationError.unimplementedType(TypeSyntax(decl.syntax.initializer.value)) + } + resolvingAliases.insert(id) + defer { resolvingAliases.remove(id) } + return try SwiftType(decl.syntax.initializer.value, lookupContext: self) + } } enum TypeLookupError: Error { diff --git a/Tests/JExtractSwiftTests/TypealiasResolutionTests.swift b/Tests/JExtractSwiftTests/TypealiasResolutionTests.swift new file mode 100644 index 000000000..9d18fcae8 --- /dev/null +++ b/Tests/JExtractSwiftTests/TypealiasResolutionTests.swift @@ -0,0 +1,408 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftJavaConfigurationShared +import Testing + +@testable import JExtractSwiftLib + +@Suite +struct TypealiasResolutionTests { + + // ==== ----------------------------------------------------------------------- + // MARK: Primitive RHS + + let primitiveAliasInput = + #""" + public typealias Amount = Double + + public struct TypealiasUser { + public var amount: Amount + + public init(amount: Amount) { + self.amount = amount + } + + public func doubled() -> Amount { + amount * 2 + } + } + + public func makeAmount(_ value: Amount) -> Amount { + value + } + """# + + @Test("typealias Amount = Double resolves so struct members are exported") + func primitiveAliasResolvesStructMembers() throws { + var config = Configuration() + config.swiftModule = "SwiftModule" + let translator = Swift2JavaTranslator(config: config) + try translator.analyze(path: "/fake/Fake.swift", text: primitiveAliasInput) + + let user = try #require(translator.importedTypes["TypealiasUser"]) + + #expect(user.variables.contains { $0.name == "amount" }, "Property `amount: Amount` should be extracted") + #expect(user.methods.contains { $0.name == "doubled" }, "Method `doubled() -> Amount` should be extracted") + #expect(!user.initializers.isEmpty, "Initializer `init(amount: Amount)` should be extracted") + } + + @Test("typealias Amount = Double also unblocks free functions") + func primitiveAliasResolvesFreeFunc() throws { + var config = Configuration() + config.swiftModule = "SwiftModule" + let translator = Swift2JavaTranslator(config: config) + try translator.analyze(path: "/fake/Fake.swift", text: primitiveAliasInput) + + #expect( + translator.importedGlobalFuncs.contains { $0.name == "makeAmount" }, + "Global func `makeAmount(_:)` should be extracted" + ) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Cross-module nominal RHS + + @Test("typealias to a Swift standard library nominal resolves") + func aliasToStandardLibraryNominalResolves() throws { + let input = + #""" + public typealias MyInt = Int64 + + public struct Holder { + public var value: MyInt + public init(value: MyInt) { self.value = value } + } + """# + + var config = Configuration() + config.swiftModule = "SwiftModule" + let translator = Swift2JavaTranslator(config: config) + try translator.analyze(path: "/fake/Fake.swift", text: input) + + let holder = try #require(translator.importedTypes["Holder"]) + #expect(holder.variables.contains { $0.name == "value" }) + #expect(!holder.initializers.isEmpty) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Generic typealias with use-site arguments + + @Test("Generic typealias substitutes type parameters at the use site") + func genericAliasSubstitutesAtUseSite() throws { + let input = + #""" + public typealias Maybe = Optional + + public func unwrapOrZero(_ value: Maybe) -> Int64 { + value ?? 0 + } + """# + + var config = Configuration() + config.swiftModule = "SwiftModule" + let translator = Swift2JavaTranslator(config: config) + try translator.analyze(path: "/fake/Fake.swift", text: input) + + let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "unwrapOrZero" }) + let paramType = try #require(fn.functionSignature.parameters.first?.type) + + // The parameter type should be Optional (substituted), preserving + // the optional sugar — i.e. it must be a nominal Optional whose first + // generic argument is Swift.Int64. + guard case .nominal(let nominal) = paramType else { + Issue.record("Expected paramType to be a nominal type, got \(paramType)") + return + } + #expect(nominal.nominalTypeDecl.name == "Optional") + let arg0 = try #require(nominal.genericArguments?.first) + #expect(arg0.description == "Int64", "Expected substituted T to be Int64, got \(arg0)") + } + + @Test("Multi-parameter generic alias substitutes each parameter independently") + func multiParameterGenericAliasSubstitutes() throws { + let input = + #""" + public typealias MyDict = Dictionary + + public func describe(_ dict: MyDict) -> String { + "\(dict)" + } + """# + + var config = Configuration() + config.swiftModule = "SwiftModule" + let translator = Swift2JavaTranslator(config: config) + try translator.analyze(path: "/fake/Fake.swift", text: input) + + let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "describe" }) + let paramType = try #require(fn.functionSignature.parameters.first?.type) + + guard case .nominal(let nominal) = paramType else { + Issue.record("Expected paramType to be a nominal type, got \(paramType)") + return + } + #expect(nominal.nominalTypeDecl.name == "Dictionary") + let args = try #require(nominal.genericArguments) + #expect(args.count == 2) + #expect(args[0].description == "String") + #expect(args[1].description == "Int64") + } + + @Test("Use-site arg count mismatch on generic alias is silently dropped") + func wrongUseSiteArgCountIsDropped() throws { + let input = + #""" + public typealias OneArg = Optional + + public struct Holder { + // Missing the type argument — invalid Swift, but jextract should + // silently drop the property rather than crash. + public var bad: OneArg + } + """# + + var config = Configuration() + config.swiftModule = "SwiftModule" + let translator = Swift2JavaTranslator(config: config) + try translator.analyze(path: "/fake/Fake.swift", text: input) + + let holder = try #require(translator.importedTypes["Holder"]) + #expect(holder.variables.isEmpty, "Property `bad: OneArg` should be dropped (arity mismatch)") + } + + // ==== ----------------------------------------------------------------------- + // MARK: Cycle detection + + @Test("Cyclic typealias chain is silently dropped, no crash") + func cyclicAliasDoesNotCrash() throws { + let input = + #""" + public typealias A = B + public typealias B = A + + public struct UsesAlias { + public var x: A + public init(x: A) { self.x = x } + } + """# + + var config = Configuration() + config.swiftModule = "SwiftModule" + let translator = Swift2JavaTranslator(config: config) + try translator.analyze(path: "/fake/Fake.swift", text: input) + + // The struct itself is still imported, but its members are dropped + // because the alias never resolves. + let usesAlias = try #require(translator.importedTypes["UsesAlias"]) + #expect(usesAlias.variables.isEmpty, "Property `x: A` should be silently dropped (cycle)") + } + + // ==== ----------------------------------------------------------------------- + // MARK: Generic specialization typealias + + @Test("Existing `typealias FishBox = Box` specialization path still fires") + func genericSpecializationWorks() throws { + let input = + #""" + public struct Fish { + public var name: String + } + + public struct Box { + public var items: [Element] + public init() { self.items = [] } + } + + public typealias FishBox = Box + """# + + var config = Configuration() + config.swiftModule = "SwiftModule" + let translator = Swift2JavaTranslator(config: config) + try translator.analyze(path: "/fake/Fake.swift", text: input) + + #expect(translator.importedTypes["FishBox"] != nil, "FishBox specialization should still register") + } + + // ==== ----------------------------------------------------------------------- + // MARK: Chained typealiases + + @Test("Typealias chain `A = B; B = C; C = Int64` resolves through all hops") + func chainedAliasesResolveAllHops() throws { + let input = + #""" + public typealias A = B + public typealias B = C + public typealias C = Int64 + + public func passA(_ x: A) -> A { x } + """# + + var config = Configuration() + config.swiftModule = "SwiftModule" + let translator = Swift2JavaTranslator(config: config) + try translator.analyze(path: "/fake/Fake.swift", text: input) + + let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "passA" }) + let paramType = try #require(fn.functionSignature.parameters.first?.type) + #expect( + paramType.description == "Int64", + "A → B → C → Int64 should fully resolve, got \(paramType)" + ) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Sugar-preserving aliases + + @Test("Typealias to optional sugar resolves to an optional") + func aliasToOptionalSugar() throws { + let input = + #""" + public typealias MaybeInt = Int64? + + public func unwrap(_ x: MaybeInt) -> Int64 { x ?? 0 } + """# + + var config = Configuration() + config.swiftModule = "SwiftModule" + let translator = Swift2JavaTranslator(config: config) + try translator.analyze(path: "/fake/Fake.swift", text: input) + + let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "unwrap" }) + let paramType = try #require(fn.functionSignature.parameters.first?.type) + + guard case .nominal(let nominal) = paramType else { + Issue.record("Expected Optional nominal, got \(paramType)") + return + } + #expect(nominal.nominalTypeDecl.name == "Optional") + #expect(nominal.genericArguments?.first?.description == "Int64") + } + + @Test("Typealias to array sugar resolves to an array") + func aliasToArraySugar() throws { + let input = + #""" + public typealias Bytes = [Int8] + + public func first(_ b: Bytes) -> Int8 { b.first ?? 0 } + """# + + var config = Configuration() + config.swiftModule = "SwiftModule" + let translator = Swift2JavaTranslator(config: config) + try translator.analyze(path: "/fake/Fake.swift", text: input) + + let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "first" }) + let paramType = try #require(fn.functionSignature.parameters.first?.type) + + guard case .nominal(let nominal) = paramType else { + Issue.record("Expected Array nominal, got \(paramType)") + return + } + #expect(nominal.nominalTypeDecl.name == "Array") + #expect(nominal.genericArguments?.first?.description == "Int8") + } + + // ==== ----------------------------------------------------------------------- + // MARK: Conditional compilation aliases + + @Test("Typealias inside `#if` resolves the active branch") + func aliasInsideIfConfigResolvesActiveBranch() throws { + // jextract's default build configuration treats every `os(...)` check as + // active, so the first `#if` clause wins. Here that's `Amount = Int64`. + let input = + #""" + #if os(Android) + public typealias Amount = Int64 + #else + public typealias Amount = Double + #endif + + public func add(_ a: Amount, _ b: Amount) -> Amount { a + b } + """# + + var config = Configuration() + config.swiftModule = "SwiftModule" + let translator = Swift2JavaTranslator(config: config) + try translator.analyze(path: "/fake/Fake.swift", text: input) + + let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "add" }) + let paramType = try #require(fn.functionSignature.parameters.first?.type) + #expect( + paramType.description == "Int64", + "Active `#if` branch should bind Amount = Int64, got \(paramType)" + ) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Aliases used as method parameter / return types inside a type + + @Test("Typealias used in a method signature resolves through the lookup") + func aliasInsideMethodSignatureResolves() throws { + let input = + #""" + public typealias Score = Int64 + + public class Player { + public var score: Score + public init(score: Score) { self.score = score } + public func bump(by delta: Score) -> Score { + score += delta + return score + } + } + """# + + var config = Configuration() + config.swiftModule = "SwiftModule" + let translator = Swift2JavaTranslator(config: config) + try translator.analyze(path: "/fake/Fake.swift", text: input) + + let player = try #require(translator.importedTypes["Player"]) + let bump = try #require(player.methods.first { $0.name == "bump" }) + let paramType = try #require(bump.functionSignature.parameters.first?.type) + #expect(paramType.description == "Int64") + #expect(bump.functionSignature.result.type.description == "Int64") + } + + // ==== ----------------------------------------------------------------------- + // MARK: Generic alias resolved transitively + + @Test("Generic typealias whose RHS is itself an alias resolves through both") + func genericAliasOverPlainAlias() throws { + let input = + #""" + public typealias Bag = Optional + public typealias IntBag = Bag + + public func openIntBag(_ b: IntBag) -> Int64 { b ?? 0 } + """# + + var config = Configuration() + config.swiftModule = "SwiftModule" + let translator = Swift2JavaTranslator(config: config) + try translator.analyze(path: "/fake/Fake.swift", text: input) + + let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "openIntBag" }) + let paramType = try #require(fn.functionSignature.parameters.first?.type) + guard case .nominal(let nominal) = paramType else { + Issue.record("Expected Optional nominal, got \(paramType)") + return + } + #expect(nominal.nominalTypeDecl.name == "Optional") + #expect(nominal.genericArguments?.first?.description == "Int64") + } +}