From 8414bf8b16b27b963973f72516a287587dfa54ae Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Fri, 14 Nov 2025 15:15:17 +0900 Subject: [PATCH] wrap-java: correct importing nested types We cannot just blindly visit all getClasses() because this includes types form super classes as well. This is useful in general, but in this context we specifically want types which are nested in this exact declaration, not its parens. --- Sources/SwiftJava/String+Extensions.swift | 3 +- .../Commands/WrapJavaCommand.swift | 27 +++++++++---- .../JavaTranslator+Validation.swift | 12 ++---- .../WrapJavaTests/BasicWrapJavaTests.swift | 39 +++++++++++++++++++ 4 files changed, 64 insertions(+), 17 deletions(-) diff --git a/Sources/SwiftJava/String+Extensions.swift b/Sources/SwiftJava/String+Extensions.swift index b45f60bfb..0af0de107 100644 --- a/Sources/SwiftJava/String+Extensions.swift +++ b/Sources/SwiftJava/String+Extensions.swift @@ -15,8 +15,7 @@ import Foundation extension String { - /// For a String that's of the form java.util.Vector, return the "Vector" - /// part. + /// For a String that's of the form java.util.Vector, return the "Vector" part. package var defaultSwiftNameForJavaClass: String { if let dotLoc = lastIndex(of: ".") { let afterDot = index(after: dotLoc) diff --git a/Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift b/Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift index ae985e772..0af1e75bb 100644 --- a/Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift +++ b/Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift @@ -180,21 +180,24 @@ extension SwiftJava.WrapJavaCommand { // of classes to be translated if they were already specified. var allClassesToVisit = javaClasses var currentClassIndex: Int = 0 - while currentClassIndex < allClassesToVisit.count { + outerClassLoop: while currentClassIndex < allClassesToVisit.count { defer { currentClassIndex += 1 } - // The current class we're in. + // The current top-level class we're in. let currentClass = allClassesToVisit[currentClassIndex] + let currentClassName = currentClass.getName() guard let currentSwiftName = translator.translatedClasses[currentClass.getName()]?.swiftType else { continue } - // Find all of the nested classes that weren't explicitly translated - // already. - let nestedClasses: [JavaClass] = currentClass.getClasses().compactMap { nestedClass in - guard let nestedClass else { return nil } + // Find all of the nested classes that weren't explicitly translated already. + let nestedAndSuperclassNestedClasses = currentClass.getClasses() // watch out, this includes nested types from superclasses + let nestedClasses: [JavaClass] = nestedAndSuperclassNestedClasses.compactMap { nestedClass in + guard let nestedClass else { + return nil + } // If this is a local class, we're done. let javaClassName = nestedClass.getName() @@ -202,6 +205,14 @@ extension SwiftJava.WrapJavaCommand { return nil } + // We only want to visit and import types which are explicitly inside this decl, + // and NOT any of the types contained in the super classes. That would violate our "current class" + // nesting, because those are *actually* nested in the other class, not "the current one" (i.e. in a super class). + guard javaClassName.hasPrefix(currentClassName) else { + log.trace("Skip super-class nested class '\(javaClassName)', is not member of \(currentClassName). Will be visited independently.") + return nil + } + // If we have an inclusive filter, import only types from it for include in config.filterInclude ?? [] { guard javaClassName.starts(with: include) else { @@ -227,7 +238,9 @@ extension SwiftJava.WrapJavaCommand { .defaultSwiftNameForJavaClass let swiftName = "\(currentSwiftName).\(swiftUnqualifiedName)" - translator.translatedClasses[javaClassName] = SwiftTypeName(module: nil, name: swiftName) + let translatedSwiftName = SwiftTypeName(module: nil, name: swiftName) + translator.translatedClasses[javaClassName] = translatedSwiftName + log.debug("Record translated Java class '\(javaClassName)' -> \(translatedSwiftName)") return nestedClass } diff --git a/Sources/SwiftJavaToolLib/JavaTranslator+Validation.swift b/Sources/SwiftJavaToolLib/JavaTranslator+Validation.swift index f56d26c5c..8071c36ad 100644 --- a/Sources/SwiftJavaToolLib/JavaTranslator+Validation.swift +++ b/Sources/SwiftJavaToolLib/JavaTranslator+Validation.swift @@ -58,10 +58,10 @@ package extension JavaTranslator { package var description: String { switch self { case .multipleClassesMappedToSameName(let swiftToJavaMapping): - """ - The following Java classes were mapped to the same Swift type name: - \(swiftToJavaMapping.map(mappingDescription(mapping:)).joined(separator: "\n")) - """ + """ + The following Java classes were mapped to the same Swift type name: + \(swiftToJavaMapping.map(mappingDescription(mapping:)).joined(separator: "\n")) + """ } } @@ -72,10 +72,6 @@ package extension JavaTranslator { } } func validateClassConfiguration() throws(ValidationError) { - // for a in translatedClasses { - // print("MAPPING = \(a.key) -> \(a.value.swiftModule?.escapedSwiftName ?? "").\(a.value.swiftType.escapedSwiftName)") - // } - // Group all classes by swift name let groupedDictionary: [SwiftTypeName: [(JavaFullyQualifiedTypeName, SwiftTypeName)]] = Dictionary(grouping: translatedClasses, by: { // SwiftTypeName(swiftType: $0.value.swiftType, swiftModule: $0.value.swiftModule) diff --git a/Tests/SwiftJavaToolLibTests/WrapJavaTests/BasicWrapJavaTests.swift b/Tests/SwiftJavaToolLibTests/WrapJavaTests/BasicWrapJavaTests.swift index d4596c901..74ed3ce28 100644 --- a/Tests/SwiftJavaToolLibTests/WrapJavaTests/BasicWrapJavaTests.swift +++ b/Tests/SwiftJavaToolLibTests/WrapJavaTests/BasicWrapJavaTests.swift @@ -49,4 +49,43 @@ final class BasicWrapJavaTests: XCTestCase { ) } + func test_wrapJava_doNotDupeImportNestedClassesFromSuperclassAutomatically() async throws { + let classpathURL = try await compileJava( + """ + package com.example; + + class SuperClass { + class SuperNested {} + } + + class ExampleSimpleClass { + class SimpleNested {} + } + """) + + try assertWrapJavaOutput( + javaClassNames: [ + "com.example.SuperClass", + "com.example.SuperClass$SuperNested", + "com.example.ExampleSimpleClass", + ], + classpath: [classpathURL], + expectedChunks: [ + """ + @JavaClass("com.example.SuperClass") + open class SuperClass: JavaObject { + """, + // FIXME: the mapping configuration could be used to nest this properly but today we don't automatically? + """ + @JavaClass("com.example.SuperClass$SuperNested") + open class SuperNested: JavaObject { + """, + """ + @JavaClass("com.example.SuperClass") + open class SuperClass: JavaObject { + """, + ] + ) + } + } \ No newline at end of file