From afc0c5a30ff53ee2c484decc25d086b0ab0a7dfa Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Thu, 16 Oct 2025 21:32:10 +0900 Subject: [PATCH 1/4] jextract: generate one output swift file per input file Rather than generating an output file per TYPE which we did before. The per-type mode cannot work correctly because SwiftPM expects to know all the output Swift files. So we'd have to parse sources and determine up front what the outputs will be -- doable, but problematic -- instead, we now generate files based on input files, and map which type goes into which output file. This makes it also easier to find where thunks are -- they are in the same named file as the original type or func was declared in. This may have some edge case problems still. Resolves https://github.com/swiftlang/swift-java/issues/365 --- .../JExtractSwiftPlugin.swift | 8 +- .../MySwiftLibrary/MultiplePublicTypes.swift | 26 ++++++ .../MySwiftLibrary/MultiplePublicTypes.swift | 26 ++++++ Sources/JExtractSwiftLib/CodePrinter.swift | 34 ++++++-- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 47 ++++++++--- .../FFM/FFMSwift2JavaGenerator.swift | 20 ++--- Sources/JExtractSwiftLib/ImportedDecls.swift | 8 +- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 47 ++++++++--- .../JNI/JNISwift2JavaGenerator.swift | 6 +- .../Swift2JavaTranslator.swift | 29 +++---- .../JExtractSwiftLib/Swift2JavaVisitor.swift | 80 +++++++++++++------ .../SwiftTypes/SwiftFunctionSignature.swift | 2 +- .../SwiftTypes/SwiftKnownModules.swift | 6 +- .../SwiftNominalTypeDeclaration.swift | 27 ++++++- .../SwiftParsedModuleSymbolTableBuilder.swift | 26 +++--- .../SwiftTypes/SwiftSymbolTable.swift | 13 +-- .../SwiftTypes/SwiftTypeLookupContext.swift | 25 +++--- .../Asserts/TextAssertions.swift | 2 +- .../FuncCallbackImportTests.swift | 6 +- .../FunctionDescriptorImportTests.swift | 4 +- .../MethodImportTests.swift | 18 ++--- .../SwiftSymbolTableTests.swift | 5 +- 22 files changed, 320 insertions(+), 145 deletions(-) create mode 100644 Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MultiplePublicTypes.swift create mode 100644 Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MultiplePublicTypes.swift diff --git a/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift b/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift index 49ed3c4d..8d0be455 100644 --- a/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift +++ b/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift @@ -93,6 +93,7 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { $0.pathExtension == "swift" } + // Output Swift files are just Java filename based converted to Swift files one-to-one var outputSwiftFiles: [URL] = swiftFiles.compactMap { sourceFileURL in guard sourceFileURL.isFileURL else { return nil as URL? @@ -102,7 +103,7 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { guard sourceFilePath.starts(with: sourceDir) else { fatalError("Could not get relative path for source file \(sourceFilePath)") } - var outputURL = outputSwiftDirectory + let outputURL = outputSwiftDirectory .appending(path: String(sourceFilePath.dropFirst(sourceDir.count).dropLast(sourceFileURL.lastPathComponent.count + 1))) let inputFileName = sourceFileURL.deletingPathExtension().lastPathComponent @@ -116,11 +117,12 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { // If the module uses 'Data' type, the thunk file is emitted as if 'Data' is declared // in that module. Declare the thunk file as the output. - // FIXME: Make this conditional. outputSwiftFiles += [ - outputSwiftDirectory.appending(path: "Data+SwiftJava.swift") + outputSwiftDirectory.appending(path: "Foundation+SwiftJava.swift") ] + print("[swift-java-plugin] Output swift files:\n - \(outputSwiftFiles.map({$0.absoluteString}).joined(separator: "\n - "))") + return [ .buildCommand( displayName: "Generate Java wrappers for Swift types", diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MultiplePublicTypes.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MultiplePublicTypes.swift new file mode 100644 index 00000000..234cb357 --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MultiplePublicTypes.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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 +// +//===----------------------------------------------------------------------===// + +// This file exists to exercise the swiftpm plugin generating separate output Java files +// for the public types; because Java public types must be in a file with the same name as the type. + +public struct PublicTypeOne { + public init() {} + public func test() {} +} + +public struct PublicTypeTwo { + public init() {} + public func test() {} +} \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MultiplePublicTypes.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MultiplePublicTypes.swift new file mode 100644 index 00000000..b7a02d78 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MultiplePublicTypes.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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 +// +//===----------------------------------------------------------------------===// + +// This file exists to exercise the swiftpm plugin generating separate output Java files +// for the public types; because Java public types must be in a file with the same name as the type. + +public struct PublicTypeOne { + public init() {} + public func test() {} +} + +public struct PublicTypeTwo { + public init() {} + public func test() {} +} \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/CodePrinter.swift b/Sources/JExtractSwiftLib/CodePrinter.swift index 8c22fbee..c5f04d51 100644 --- a/Sources/JExtractSwiftLib/CodePrinter.swift +++ b/Sources/JExtractSwiftLib/CodePrinter.swift @@ -216,14 +216,28 @@ extension CodePrinter { /// - Returns: the output path of the generated file, if any (i.e. not in accumulate in memory mode) package mutating func writeContents( - outputDirectory: String, + outputDirectory _outputDirectory: String, javaPackagePath: String?, - filename: String + filename _filename: String ) throws -> URL? { + + // We handle 'filename' that has a path, since that simplifies passing paths from root output directory enourmously. + // This just moves the directory parts into the output directory part in order for us to create the sub-directories. + let outputDirectory: String + let filename: String + if _filename.contains(PATH_SEPARATOR) { + let parts = _filename.split(separator: PATH_SEPARATOR) + outputDirectory = _outputDirectory.appending(PATH_SEPARATOR).appending(parts.dropLast().joined(separator: PATH_SEPARATOR)) + filename = "\(parts.last!)" + } else { + outputDirectory = _outputDirectory + filename = _filename + } + guard self.mode != .accumulateAll else { // if we're accumulating everything, we don't want to finalize/flush any contents // let's mark that this is where a write would have happened though: - print("// ^^^^ Contents of: \(outputDirectory)/\(filename)") + print("// ^^^^ Contents of: \(outputDirectory)\(PATH_SEPARATOR)\(filename)") return nil } @@ -233,7 +247,7 @@ extension CodePrinter { "// ==== ---------------------------------------------------------------------------------------------------" ) if let javaPackagePath { - print("// \(javaPackagePath)/\(filename)") + print("// \(javaPackagePath)\(PATH_SEPARATOR)\(filename)") } else { print("// \(filename)") } @@ -242,9 +256,15 @@ extension CodePrinter { } let targetDirectory = [outputDirectory, javaPackagePath].compactMap { $0 }.joined(separator: PATH_SEPARATOR) - log.trace("Prepare target directory: \(targetDirectory)") - try FileManager.default.createDirectory( - atPath: targetDirectory, withIntermediateDirectories: true) + log.debug("Prepare target directory: '\(targetDirectory)' for file \(filename.bold)") + do { + try FileManager.default.createDirectory( + atPath: targetDirectory, withIntermediateDirectories: true) + } catch { + // log and throw since it can be confusing what the reason for failing the write was otherwise + log.warning("Failed to create directory: \(targetDirectory)") + throw error + } let outputPath = Foundation.URL(fileURLWithPath: targetDirectory).appendingPathComponent(filename) try contents.write( diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift index e43ea4c5..5ef266c8 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift @@ -22,8 +22,16 @@ extension FFMSwift2JavaGenerator { } package func writeSwiftExpectedEmptySources() throws { + let pendingFileCount = self.expectedOutputSwiftFiles.count + guard pendingFileCount > 0 else { + return // no need to write any empty files, yay + } + + print("[swift-java] Write empty [\(self.expectedOutputSwiftFiles.count)] 'expected' files in: \(swiftOutputDirectory)/") + for expectedFileName in self.expectedOutputSwiftFiles { - log.trace("Write empty file: \(expectedFileName) ...") + log.debug("Write SwiftPM-'expected' empty file: \(expectedFileName.bold)") + var printer = CodePrinter() printer.print("// Empty file generated on purpose") @@ -46,7 +54,7 @@ extension FFMSwift2JavaGenerator { outputDirectory: self.swiftOutputDirectory, javaPackagePath: nil, filename: moduleFilename) { - print("[swift-java] Generated: \(moduleFilenameBase.bold).swift (at \(outputFile))") + log.info("Generated: \(moduleFilenameBase.bold).swift (at \(outputFile.absoluteString))") self.expectedOutputSwiftFiles.remove(moduleFilename) } } catch { @@ -54,24 +62,41 @@ extension FFMSwift2JavaGenerator { } // === All types - // FIXME: write them all into the same file they were declared from +SwiftJava - for (_, ty) in self.analysis.importedTypes.sorted(by: { (lhs, rhs) in lhs.key < rhs.key }) { - let fileNameBase = "\(ty.swiftNominal.qualifiedName)+SwiftJava" - let filename = "\(fileNameBase).swift" - log.debug("Printing contents: \(filename)") + // We have to write all types to their corresponding output file that matches the file they were declared in, + // because otherwise SwiftPM plugins will not pick up files apropriately -- we expect 1 output +SwiftJava.swift file for every input. + for group: (key: String, value: [Dictionary.Element]) in Dictionary(grouping: self.analysis.importedTypes, by: { $0.value.sourceFilePath }) { + log.warning("Writing types in file group: \(group.key): \(group.value.map(\.key))") + + let importedTypesForThisFile = group.value + .map(\.value) + .sorted(by: { $0.qualifiedName < $1.qualifiedName }) + + let inputFileName = "\(group.key)".split(separator: "/").last ?? "__Unknown.swift" + let filename = "\(inputFileName)".replacing(".swift", with: "+SwiftJava.swift") + + for ty in importedTypesForThisFile { + log.info("Printing Swift thunks for type: \(ty.qualifiedName.bold)") + printer.printSeparator("Thunks for \(ty.qualifiedName)") + + do { + try printSwiftThunkSources(&printer, ty: ty) + } catch { + log.warning("Failed to print to Swift thunks for type'\(ty.qualifiedName)' to '\(filename)', error: \(error)") + } + + } + log.warning("Write Swift thunks file: \(filename.bold)") do { - try printSwiftThunkSources(&printer, ty: ty) - if let outputFile = try printer.writeContents( outputDirectory: self.swiftOutputDirectory, javaPackagePath: nil, filename: filename) { - print("[swift-java] Generated: \(fileNameBase.bold).swift (at \(outputFile))") + log.info("Done writing Swift thunks to: \(outputFile.absoluteString)") self.expectedOutputSwiftFiles.remove(filename) } } catch { - log.warning("Failed to write to Swift thunks: \(filename)") + log.warning("Failed to write to Swift thunks: \(filename), error: \(error)") } } } diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift index c9a5028b..f9ba88b9 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift @@ -60,16 +60,14 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator { // If we are forced to write empty files, construct the expected outputs if translator.config.writeEmptyFiles ?? false { self.expectedOutputSwiftFiles = Set(translator.inputs.compactMap { (input) -> String? in - guard let filePathPart = input.filePath.split(separator: "/\(translator.swiftModuleName)/").last else { + guard let filePathPart = input.path.split(separator: "/\(translator.swiftModuleName)/").last else { return nil } return String(filePathPart.replacing(".swift", with: "+SwiftJava.swift")) }) self.expectedOutputSwiftFiles.insert("\(translator.swiftModuleName)Module+SwiftJava.swift") - - // FIXME: Can we avoid this? - self.expectedOutputSwiftFiles.insert("Data+SwiftJava.swift") + self.expectedOutputSwiftFiles.insert("Foundation+SwiftJava.swift") } else { self.expectedOutputSwiftFiles = [] } @@ -77,16 +75,12 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator { func generate() throws { try writeSwiftThunkSources() - print("[swift-java] Generated Swift sources (module: '\(self.swiftModuleName)') in: \(swiftOutputDirectory)/") + log.info("Generated Swift sources (module: '\(self.swiftModuleName)') in: \(swiftOutputDirectory)/") try writeExportedJavaSources() - print("[swift-java] Generated Java sources (package: '\(javaPackage)') in: \(javaOutputDirectory)/") + log.info("Generated Java sources (package: '\(javaPackage)') in: \(javaOutputDirectory)/") - let pendingFileCount = self.expectedOutputSwiftFiles.count - if pendingFileCount > 0 { - print("[swift-java] Write empty [\(pendingFileCount)] 'expected' files in: \(swiftOutputDirectory)/") - try writeSwiftExpectedEmptySources() - } + try writeSwiftExpectedEmptySources() } } @@ -134,7 +128,7 @@ extension FFMSwift2JavaGenerator { javaPackagePath: javaPackagePath, filename: filename ) { - print("[swift-java] Generated: \(ty.swiftNominal.name.bold).java (at \(outputFile))") + log.info("Generated: \((ty.swiftNominal.name.bold + ".java").bold) (at \(outputFile.absoluteString))") } } @@ -148,7 +142,7 @@ extension FFMSwift2JavaGenerator { javaPackagePath: javaPackagePath, filename: filename) { - print("[swift-java] Generated: \(self.swiftModuleName).java (at \(outputFile))") + log.info("Generated: \((self.swiftModuleName + ".java").bold) (at \(outputFile.absoluteString))") } } } diff --git a/Sources/JExtractSwiftLib/ImportedDecls.swift b/Sources/JExtractSwiftLib/ImportedDecls.swift index 7a071d9a..1f0d3efc 100644 --- a/Sources/JExtractSwiftLib/ImportedDecls.swift +++ b/Sources/JExtractSwiftLib/ImportedDecls.swift @@ -27,9 +27,15 @@ package enum SwiftAPIKind { /// Describes a Swift nominal type (e.g., a class, struct, enum) that has been /// imported and is being translated into Java. -package class ImportedNominalType: ImportedDecl { +package final class ImportedNominalType: ImportedDecl { let swiftNominal: SwiftNominalTypeDeclaration + // The short path from module root to the file in which this nominal was originally declared. + // E.g. for `Sources/Example/My/Types.swift` it would be `My/Types.swift`. + package var sourceFilePath: String { + self.swiftNominal.sourceFilePath + } + package var initializers: [ImportedFunc] = [] package var methods: [ImportedFunc] = [] package var variables: [ImportedFunc] = [] diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 3848ceac..1d4f81dc 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -26,8 +26,16 @@ extension JNISwift2JavaGenerator { } package func writeSwiftExpectedEmptySources() throws { + let pendingFileCount = self.expectedOutputSwiftFiles.count + guard pendingFileCount > 0 else { + return // no need to write any empty files, yay + } + + print("[swift-java] Write empty [\(self.expectedOutputSwiftFiles.count)] 'expected' files in: \(swiftOutputDirectory)/") + for expectedFileName in self.expectedOutputSwiftFiles { - logger.trace("Write empty file: \(expectedFileName) ...") + logger.debug("Write SwiftPM-'expected' empty file: \(expectedFileName.bold)") + var printer = CodePrinter() printer.print("// Empty file generated on purpose") @@ -52,27 +60,46 @@ extension JNISwift2JavaGenerator { javaPackagePath: nil, filename: moduleFilename ) { - print("[swift-java] Generated: \(moduleFilenameBase.bold).swift (at \(outputFile))") + logger.info("Generated: \(moduleFilenameBase.bold).swift (at \(outputFile.absoluteString))") self.expectedOutputSwiftFiles.remove(moduleFilename) } - for (_, ty) in self.analysis.importedTypes.sorted(by: { (lhs, rhs) in lhs.key < rhs.key }) { - let fileNameBase = "\(ty.swiftNominal.qualifiedName)+SwiftJava" - let filename = "\(fileNameBase).swift" - logger.debug("Printing contents: \(filename)") + // === All types + // We have to write all types to their corresponding output file that matches the file they were declared in, + // because otherwise SwiftPM plugins will not pick up files apropriately -- we expect 1 output +SwiftJava.swift file for every input. + for group: (key: String, value: [Dictionary.Element]) in Dictionary(grouping: self.analysis.importedTypes, by: { $0.value.sourceFilePath }) { + logger.warning("Writing types in file group: \(group.key): \(group.value.map(\.key))") - do { - try printNominalTypeThunks(&printer, ty) + let importedTypesForThisFile = group.value + .map(\.value) + .sorted(by: { $0.qualifiedName < $1.qualifiedName }) + let inputFileName = "\(group.key)".split(separator: "/").last ?? "__Unknown.swift" + let filename = "\(inputFileName)".replacing(".swift", with: "+SwiftJava.swift") + + for ty in importedTypesForThisFile { + logger.info("Printing Swift thunks for type: \(ty.qualifiedName.bold)") + printer.printSeparator("Thunks for \(ty.qualifiedName)") + + do { + try printNominalTypeThunks(&printer, ty) + } catch { + logger.warning("Failed to print to Swift thunks for type'\(ty.qualifiedName)' to '\(filename)', error: \(error)") + } + + } + + logger.warning("Write Swift thunks file: \(filename.bold)") + do { if let outputFile = try printer.writeContents( outputDirectory: self.swiftOutputDirectory, javaPackagePath: nil, filename: filename) { - print("[swift-java] Generated: \(fileNameBase.bold).swift (at \(outputFile))") + logger.info("Done writing Swift thunks to: \(outputFile.absoluteString)") self.expectedOutputSwiftFiles.remove(filename) } } catch { - logger.warning("Failed to write to Swift thunks: \(filename)") + logger.warning("Failed to write to Swift thunks: \(filename), error: \(error)") } } } catch { diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index a677bcde..3b84cfb9 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -67,16 +67,14 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { // If we are forced to write empty files, construct the expected outputs if translator.config.writeEmptyFiles ?? false { self.expectedOutputSwiftFiles = Set(translator.inputs.compactMap { (input) -> String? in - guard let filePathPart = input.filePath.split(separator: "/\(translator.swiftModuleName)/").last else { + guard let filePathPart = input.path.split(separator: "/\(translator.swiftModuleName)/").last else { return nil } return String(filePathPart.replacing(".swift", with: "+SwiftJava.swift")) }) self.expectedOutputSwiftFiles.insert("\(translator.swiftModuleName)Module+SwiftJava.swift") - - // FIXME: Can we avoid this? - self.expectedOutputSwiftFiles.insert("Data+SwiftJava.swift") + self.expectedOutputSwiftFiles.insert("Foundation+SwiftJava.swift") } else { self.expectedOutputSwiftFiles = [] } diff --git a/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift b/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift index 9dcba340..7de792c0 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift @@ -32,12 +32,7 @@ public final class Swift2JavaTranslator { // ==== Input - struct Input { - let filePath: String - let syntax: SourceFileSyntax - } - - var inputs: [Input] = [] + var inputs: [SwiftJavaInputFile] = [] /// A list of used Swift class names that live in dependencies, e.g. `JavaInteger` package var dependenciesClasses: [String] = [] @@ -85,15 +80,12 @@ extension Swift2JavaTranslator { package func add(filePath: String, text: String) { log.trace("Adding: \(filePath)") let sourceFileSyntax = Parser.parse(source: text) - self.inputs.append(Input(filePath: filePath, syntax: sourceFileSyntax)) + self.inputs.append(SwiftJavaInputFile(syntax: sourceFileSyntax, path: filePath)) } /// Convenient method for analyzing single file. - package func analyze( - file: String, - text: String - ) throws { - self.add(filePath: file, text: text) + package func analyze(path: String, text: String) throws { + self.add(filePath: path, text: text) try self.analyze() } @@ -104,8 +96,8 @@ extension Swift2JavaTranslator { let visitor = Swift2JavaVisitor(translator: self) for input in self.inputs { - log.trace("Analyzing \(input.filePath)") - visitor.visit(sourceFile: input.syntax) + log.trace("Analyzing \(input.path)") + visitor.visit(inputFile: input) } // If any API uses 'Foundation.Data' or 'FoundationEssentials.Data', @@ -113,7 +105,7 @@ extension Swift2JavaTranslator { if let dataDecl = self.symbolTable[.foundationData] ?? self.symbolTable[.essentialsData] { let dataProtocolDecl = (self.symbolTable[.foundationDataProtocol] ?? self.symbolTable[.essentialsDataProtocol])! if self.isUsing(where: { $0 == dataDecl || $0 == dataProtocolDecl }) { - visitor.visit(nominalDecl: dataDecl.syntax!.asNominal!, in: nil) + visitor.visit(nominalDecl: dataDecl.syntax!.asNominal!, in: nil, sourceFilePath: "Foundation/FAKE_FOUNDATION_DATA.swift") } } } @@ -123,7 +115,7 @@ extension Swift2JavaTranslator { let symbolTable = SwiftSymbolTable.setup( moduleName: self.swiftModuleName, - inputs.map({ $0.syntax }) + [dependenciesSource], + inputs + [dependenciesSource], log: self.log ) self.lookupContext = SwiftTypeLookupContext(symbolTable: symbolTable) @@ -184,13 +176,14 @@ extension Swift2JavaTranslator { } /// Returns a source file that contains all the available dependency classes. - private func buildDependencyClassesSourceFile() -> SourceFileSyntax { + private func buildDependencyClassesSourceFile() -> SwiftJavaInputFile { let contents = self.dependenciesClasses.map { "public class \($0) {}" } .joined(separator: "\n") - return SourceFileSyntax(stringLiteral: contents) + let syntax = SourceFileSyntax(stringLiteral: contents) + return SwiftJavaInputFile(syntax: syntax, path: "FakeDependencyClassesSourceFile.swift") } } diff --git a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift index 13185a5c..247b2662 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift @@ -29,41 +29,42 @@ final class Swift2JavaVisitor { var log: Logger { translator.log } - func visit(sourceFile node: SourceFileSyntax) { + func visit(inputFile: SwiftJavaInputFile) { + let node = inputFile.syntax for codeItem in node.statements { if let declNode = codeItem.item.as(DeclSyntax.self) { - self.visit(decl: declNode, in: nil) + self.visit(decl: declNode, in: nil, sourceFilePath: inputFile.path) } } } - func visit(decl node: DeclSyntax, in parent: ImportedNominalType?) { + func visit(decl node: DeclSyntax, in parent: ImportedNominalType?, sourceFilePath: String) { switch node.as(DeclSyntaxEnum.self) { case .actorDecl(let node): - self.visit(nominalDecl: node, in: parent) + self.visit(nominalDecl: node, in: parent, sourceFilePath: sourceFilePath) case .classDecl(let node): - self.visit(nominalDecl: node, in: parent) + self.visit(nominalDecl: node, in: parent, sourceFilePath: sourceFilePath) case .structDecl(let node): - self.visit(nominalDecl: node, in: parent) + self.visit(nominalDecl: node, in: parent, sourceFilePath: sourceFilePath) case .enumDecl(let node): - self.visit(enumDecl: node, in: parent) + self.visit(enumDecl: node, in: parent, sourceFilePath: sourceFilePath) case .protocolDecl(let node): - self.visit(nominalDecl: node, in: parent) + self.visit(nominalDecl: node, in: parent, sourceFilePath: sourceFilePath) case .extensionDecl(let node): - self.visit(extensionDecl: node, in: parent) + self.visit(extensionDecl: node, in: parent, sourceFilePath: sourceFilePath) case .typeAliasDecl: break // TODO: Implement; https://github.com/swiftlang/swift-java/issues/338 case .associatedTypeDecl: - break // TODO: Implement + break // TODO: Implement associated types case .initializerDecl(let node): self.visit(initializerDecl: node, in: parent) case .functionDecl(let node): - self.visit(functionDecl: node, in: parent) + self.visit(functionDecl: node, in: parent, sourceFilePath: sourceFilePath) case .variableDecl(let node): - self.visit(variableDecl: node, in: parent) + self.visit(variableDecl: node, in: parent, sourceFilePath: sourceFilePath) case .subscriptDecl: - // TODO: Implement + // TODO: Implement subscripts break case .enumCaseDecl(let node): self.visit(enumCaseDecl: node, in: parent) @@ -75,23 +76,32 @@ final class Swift2JavaVisitor { func visit( nominalDecl node: some DeclSyntaxProtocol & DeclGroupSyntax & NamedDeclSyntax & WithAttributesSyntax & WithModifiersSyntax, - in parent: ImportedNominalType? + in parent: ImportedNominalType?, + sourceFilePath: String ) { guard let importedNominalType = translator.importedNominalType(node, parent: parent) else { return } for memberItem in node.memberBlock.members { - self.visit(decl: memberItem.decl, in: importedNominalType) + self.visit(decl: memberItem.decl, in: importedNominalType, sourceFilePath: sourceFilePath) } } - func visit(enumDecl node: EnumDeclSyntax, in parent: ImportedNominalType?) { - self.visit(nominalDecl: node, in: parent) + func visit( + enumDecl node: EnumDeclSyntax, + in parent: ImportedNominalType?, + sourceFilePath: String + ) { + self.visit(nominalDecl: node, in: parent, sourceFilePath: sourceFilePath) self.synthesizeRawRepresentableConformance(enumDecl: node, in: parent) } - func visit(extensionDecl node: ExtensionDeclSyntax, in parent: ImportedNominalType?) { + func visit( + extensionDecl node: ExtensionDeclSyntax, + in parent: ImportedNominalType?, + sourceFilePath: String + ) { guard parent == nil else { // 'extension' in a nominal type is invalid. Ignore return @@ -100,11 +110,15 @@ final class Swift2JavaVisitor { return } for memberItem in node.memberBlock.members { - self.visit(decl: memberItem.decl, in: importedNominalType) + self.visit(decl: memberItem.decl, in: importedNominalType, sourceFilePath: sourceFilePath) } } - func visit(functionDecl node: FunctionDeclSyntax, in typeContext: ImportedNominalType?) { + func visit( + functionDecl node: FunctionDeclSyntax, + in typeContext: ImportedNominalType?, + sourceFilePath: String + ) { guard node.shouldExtract(config: config, log: log, in: typeContext) else { return } @@ -139,7 +153,10 @@ final class Swift2JavaVisitor { } } - func visit(enumCaseDecl node: EnumCaseDeclSyntax, in typeContext: ImportedNominalType?) { + func visit( + enumCaseDecl node: EnumCaseDeclSyntax, + in typeContext: ImportedNominalType? + ) { guard let typeContext else { self.log.info("Enum case must be within a current type; \(node)") return @@ -182,7 +199,11 @@ final class Swift2JavaVisitor { } } - func visit(variableDecl node: VariableDeclSyntax, in typeContext: ImportedNominalType?) { + func visit( + variableDecl node: VariableDeclSyntax, + in typeContext: ImportedNominalType?, + sourceFilePath: String + ) { guard node.shouldExtract(config: config, log: log, in: typeContext) else { return } @@ -232,7 +253,10 @@ final class Swift2JavaVisitor { } } - func visit(initializerDecl node: InitializerDeclSyntax, in typeContext: ImportedNominalType?) { + func visit( + initializerDecl node: InitializerDeclSyntax, + in typeContext: ImportedNominalType?, + ) { guard let typeContext else { self.log.info("Initializer must be within a current type; \(node)") return @@ -265,7 +289,10 @@ final class Swift2JavaVisitor { typeContext.initializers.append(imported) } - private func synthesizeRawRepresentableConformance(enumDecl node: EnumDeclSyntax, in parent: ImportedNominalType?) { + private func synthesizeRawRepresentableConformance( + enumDecl node: EnumDeclSyntax, + in parent: ImportedNominalType? + ) { guard let imported = translator.importedNominalType(node, parent: parent) else { return } @@ -279,14 +306,15 @@ final class Swift2JavaVisitor { { if !imported.variables.contains(where: { $0.name == "rawValue" && $0.functionSignature.result.type != inheritanceType }) { let decl: DeclSyntax = "public var rawValue: \(raw: inheritanceType.description) { get }" - self.visit(decl: decl, in: imported) + self.visit(decl: decl, in: imported, sourceFilePath: imported.sourceFilePath) } + // FIXME: why is this un-used imported.variables.first?.signatureString if !imported.initializers.contains(where: { $0.functionSignature.parameters.count == 1 && $0.functionSignature.parameters.first?.parameterName == "rawValue" && $0.functionSignature.parameters.first?.type == inheritanceType }) { let decl: DeclSyntax = "public init?(rawValue: \(raw: inheritanceType))" - self.visit(decl: decl, in: imported) + self.visit(decl: decl, in: imported, sourceFilePath: imported.sourceFilePath) } } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift index a5b01bee..7e0f4885 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift @@ -195,7 +195,7 @@ extension SwiftFunctionSignature { guard parameterNode.specifier == nil else { throw SwiftFunctionTranslationError.genericParameterSpecifier(parameterNode) } - let param = try lookupContext.typeDeclaration(for: parameterNode) as! SwiftGenericParameterDeclaration + let param = try lookupContext.typeDeclaration(for: parameterNode, sourceFilePath: "FIXME_HAS_NO_PATH.swift") as! SwiftGenericParameterDeclaration params.append(param) if let inheritedNode = parameterNode.inheritedType { let inherited = try SwiftType(inheritedNode, lookupContext: lookupContext) diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift index 45d5df5a..7acb1199 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift @@ -43,7 +43,7 @@ enum SwiftKnownModule: String { private var swiftSymbolTable: SwiftModuleSymbolTable { var builder = SwiftParsedModuleSymbolTableBuilder(moduleName: "Swift", importedModules: [:]) - builder.handle(sourceFile: swiftSourceFile) + builder.handle(sourceFile: swiftSourceFile, sourceFilePath: "SwiftStdlib.swift") // FIXME: missing path here return builder.finalize() } @@ -53,7 +53,7 @@ private var foundationEssentialsSymbolTable: SwiftModuleSymbolTable { requiredAvailablityOfModuleWithName: "FoundationEssentials", alternativeModules: .init(isMainSourceOfSymbols: false, moduleNames: ["Foundation"]), importedModules: ["Swift": swiftSymbolTable]) - builder.handle(sourceFile: foundationEssentialsSourceFile) + builder.handle(sourceFile: foundationEssentialsSourceFile, sourceFilePath: "FakeFoundation.swift") return builder.finalize() } @@ -62,7 +62,7 @@ private var foundationSymbolTable: SwiftModuleSymbolTable { moduleName: "Foundation", alternativeModules: .init(isMainSourceOfSymbols: true, moduleNames: ["FoundationEssentials"]), importedModules: ["Swift": swiftSymbolTable]) - builder.handle(sourceFile: foundationSourceFile) + builder.handle(sourceFile: foundationSourceFile, sourceFilePath: "Foundation.swift") return builder.finalize() } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift index 763a5da2..0b8b5651 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift @@ -19,20 +19,37 @@ import SwiftSyntax public typealias NominalTypeDeclSyntaxNode = any DeclGroupSyntax & NamedDeclSyntax & WithAttributesSyntax & WithModifiersSyntax package class SwiftTypeDeclaration { + + // The short path from module root to the file in which this nominal was originally declared. + // E.g. for `Sources/Example/My/Types.swift` it would be `My/Types.swift`. + let sourceFilePath: String + /// The module in which this nominal type is defined. If this is a nested type, the /// module might be different from that of the parent type, if this nominal type /// is defined in an extension within another module. let moduleName: String - /// The name of this nominal type, e.g., 'MyCollection'. + /// The name of this nominal type, e.g. 'MyCollection'. let name: String - init(moduleName: String, name: String) { + init(sourceFilePath: String, moduleName: String, name: String) { + self.sourceFilePath = sourceFilePath self.moduleName = moduleName self.name = name } } +/// A syntax node paired with a simple file path +package struct SwiftJavaInputFile { + let syntax: SourceFileSyntax + /// Simple file path of the file from which the syntax node was parsed. + let path: String + package init(syntax: SourceFileSyntax, path: String) { + self.syntax = syntax + self.path = path + } +} + /// Describes a nominal type declaration, which can be of any kind (class, struct, etc.) /// and has a name, parent type (if nested), and owning module. package class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { @@ -66,6 +83,7 @@ package class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { /// Create a nominal type declaration from the syntax node for a nominal type /// declaration. init( + sourceFilePath: String, moduleName: String, parent: SwiftNominalTypeDeclaration?, node: NominalTypeDeclSyntaxNode @@ -82,7 +100,7 @@ package class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { case .structDecl: self.kind = .struct default: fatalError("Not a nominal type declaration") } - super.init(moduleName: moduleName, name: node.name.text) + super.init(sourceFilePath: sourceFilePath, moduleName: moduleName, name: node.name.text) } lazy var firstInheritanceType: TypeSyntax? = { @@ -145,11 +163,12 @@ package class SwiftGenericParameterDeclaration: SwiftTypeDeclaration { let syntax: GenericParameterSyntax init( + sourceFilePath: String, moduleName: String, node: GenericParameterSyntax ) { self.syntax = node - super.init(moduleName: moduleName, name: node.name.text) + super.init(sourceFilePath: sourceFilePath, moduleName: moduleName, name: node.name.text) } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift index 8abb21f5..c5586410 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift @@ -50,7 +50,8 @@ struct SwiftParsedModuleSymbolTableBuilder { extension SwiftParsedModuleSymbolTableBuilder { mutating func handle( - sourceFile: SourceFileSyntax + sourceFile: SourceFileSyntax, + sourceFilePath: String ) { // Find top-level type declarations. for statement in sourceFile.statements { @@ -60,10 +61,10 @@ extension SwiftParsedModuleSymbolTableBuilder { } if let nominalTypeNode = decl.asNominal { - self.handle(nominalTypeDecl: nominalTypeNode, parent: nil) + self.handle(sourceFilePath: sourceFilePath, nominalTypeDecl: nominalTypeNode, parent: nil) } if let extensionNode = decl.as(ExtensionDeclSyntax.self) { - self.handle(extensionDecl: extensionNode) + self.handle(extensionDecl: extensionNode, sourceFilePath: sourceFilePath) } } } @@ -71,6 +72,7 @@ extension SwiftParsedModuleSymbolTableBuilder { /// Add a nominal type declaration and all of the nested types within it to the symbol /// table. mutating func handle( + sourceFilePath: String, nominalTypeDecl node: NominalTypeDeclSyntaxNode, parent: SwiftNominalTypeDeclaration? ) { @@ -83,6 +85,7 @@ extension SwiftParsedModuleSymbolTableBuilder { // Otherwise, create the nominal type declaration. let nominalTypeDecl = SwiftNominalTypeDeclaration( + sourceFilePath: sourceFilePath, moduleName: moduleName, parent: parent, node: node @@ -96,26 +99,28 @@ extension SwiftParsedModuleSymbolTableBuilder { symbolTable.topLevelTypes[nominalTypeDecl.name] = nominalTypeDecl } - self.handle(memberBlock: node.memberBlock, parent: nominalTypeDecl) + self.handle(sourceFilePath: sourceFilePath, memberBlock: node.memberBlock, parent: nominalTypeDecl) } mutating func handle( + sourceFilePath: String, memberBlock node: MemberBlockSyntax, parent: SwiftNominalTypeDeclaration ) { for member in node.members { // Find any nested types within this nominal type and add them. if let nominalMember = member.decl.asNominal { - self.handle(nominalTypeDecl: nominalMember, parent: parent) + self.handle(sourceFilePath: sourceFilePath, nominalTypeDecl: nominalMember, parent: parent) } } } mutating func handle( - extensionDecl node: ExtensionDeclSyntax + extensionDecl node: ExtensionDeclSyntax, + sourceFilePath: String ) { - if !self.tryHandle(extension: node) { + if !self.tryHandle(extension: node, sourceFilePath: sourceFilePath) { self.unresolvedExtensions.append(node) } } @@ -123,7 +128,8 @@ extension SwiftParsedModuleSymbolTableBuilder { /// Add any nested types within the given extension to the symbol table. /// If the extended nominal type can't be resolved, returns false. mutating func tryHandle( - extension node: ExtensionDeclSyntax + extension node: ExtensionDeclSyntax, + sourceFilePath: String ) -> Bool { // Try to resolve the type referenced by this extension declaration. // If it fails, we'll try again later. @@ -141,7 +147,7 @@ extension SwiftParsedModuleSymbolTableBuilder { } // Find any nested types within this extension and add them. - self.handle(memberBlock: node.memberBlock, parent: extendedNominal) + self.handle(sourceFilePath: sourceFilePath, memberBlock: node.memberBlock, parent: extendedNominal) return true } @@ -158,7 +164,7 @@ extension SwiftParsedModuleSymbolTableBuilder { while !unresolvedExtensions.isEmpty { var extensions = self.unresolvedExtensions extensions.removeAll(where: { - self.tryHandle(extension: $0) + self.tryHandle(extension: $0, sourceFilePath: "FIXME_MISSING_FILEPATH.swift") // FIXME: missing filepath here in finalize }) // If we didn't resolve anything, we're done. diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift index 4271e297..ef299bab 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift @@ -39,7 +39,7 @@ extension SwiftSymbolTableProtocol { package class SwiftSymbolTable { let importedModules: [String: SwiftModuleSymbolTable] - let parsedModule:SwiftModuleSymbolTable + let parsedModule: SwiftModuleSymbolTable private var knownTypeToNominal: [SwiftKnownTypeDeclKind: SwiftNominalTypeDeclaration] = [:] private var prioritySortedImportedModules: [SwiftModuleSymbolTable] { @@ -55,15 +55,16 @@ package class SwiftSymbolTable { extension SwiftSymbolTable { package static func setup( moduleName: String, - _ sourceFiles: some Collection, + _ inputFiles: some Collection, log: Logger ) -> SwiftSymbolTable { // Prepare imported modules. // FIXME: Support arbitrary dependencies. var modules: Set = [] - for sourceFile in sourceFiles { - modules.formUnion(importingModules(sourceFile: sourceFile)) + for inputFile in inputFiles { + let importedModules = importingModules(sourceFile: inputFile.syntax) + modules.formUnion(importedModules) } var importedModules: [String: SwiftModuleSymbolTable] = [:] importedModules[SwiftKnownModule.swift.name] = SwiftKnownModule.swift.symbolTable @@ -84,8 +85,8 @@ extension SwiftSymbolTable { var builder = SwiftParsedModuleSymbolTableBuilder(moduleName: moduleName, importedModules: importedModules, log: log) // First, register top-level and nested nominal types to the symbol table. - for sourceFile in sourceFiles { - builder.handle(sourceFile: sourceFile) + for sourceFile in inputFiles { + builder.handle(sourceFile: sourceFile.syntax, sourceFilePath: sourceFile.path) } let parsedModule = builder.finalize() return SwiftSymbolTable(parsedModule: parsedModule, importedModules: importedModules) diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift index 9ede2b1b..f47669b5 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift @@ -48,7 +48,7 @@ class SwiftTypeLookupContext { } case .lookInMembers(let scopeNode): - if let nominalDecl = try typeDeclaration(for: 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) { return found } @@ -74,9 +74,9 @@ class SwiftTypeLookupContext { for name in names { switch name { case .identifier(let identifiableSyntax, _): - return try? typeDeclaration(for: identifiableSyntax) + return try? typeDeclaration(for: identifiableSyntax, sourceFilePath: "FIXME_NO_PATH.swift") // FIXME: how to get path here? case .declaration(let namedDeclSyntax): - return try? typeDeclaration(for: namedDeclSyntax) + return try? typeDeclaration(for: namedDeclSyntax, sourceFilePath: "FIXME_NO_PATH.swift") // FIXME: how to get path here? case .implicit(let implicitDecl): // TODO: Implement _ = implicitDecl @@ -90,7 +90,7 @@ class SwiftTypeLookupContext { /// Returns the type declaration object associated with the `Syntax` node. /// If there's no declaration created, create an instance on demand, and cache it. - func typeDeclaration(for node: some SyntaxProtocol) throws -> SwiftTypeDeclaration? { + func typeDeclaration(for node: some SyntaxProtocol, sourceFilePath: String) throws -> SwiftTypeDeclaration? { if let found = typeDecls[node.id] { return found } @@ -98,17 +98,17 @@ class SwiftTypeLookupContext { let typeDecl: SwiftTypeDeclaration switch Syntax(node).as(SyntaxEnum.self) { case .genericParameter(let node): - typeDecl = SwiftGenericParameterDeclaration(moduleName: symbolTable.moduleName, node: node) + typeDecl = SwiftGenericParameterDeclaration(sourceFilePath: sourceFilePath, moduleName: symbolTable.moduleName, node: node) case .classDecl(let node): - typeDecl = try nominalTypeDeclaration(for: node) + typeDecl = try nominalTypeDeclaration(for: node, sourceFilePath: sourceFilePath) case .actorDecl(let node): - typeDecl = try nominalTypeDeclaration(for: node) + typeDecl = try nominalTypeDeclaration(for: node, sourceFilePath: sourceFilePath) case .structDecl(let node): - typeDecl = try nominalTypeDeclaration(for: node) + typeDecl = try nominalTypeDeclaration(for: node, sourceFilePath: sourceFilePath) case .enumDecl(let node): - typeDecl = try nominalTypeDeclaration(for: node) + typeDecl = try nominalTypeDeclaration(for: node, sourceFilePath: sourceFilePath) case .protocolDecl(let node): - typeDecl = try nominalTypeDeclaration(for: node) + typeDecl = try nominalTypeDeclaration(for: node, sourceFilePath: sourceFilePath) case .typeAliasDecl: fatalError("typealias not implemented") case .associatedTypeDecl: @@ -122,8 +122,9 @@ class SwiftTypeLookupContext { } /// Create a nominal type declaration instance for the specified syntax node. - private func nominalTypeDeclaration(for node: NominalTypeDeclSyntaxNode) throws -> SwiftNominalTypeDeclaration { + private func nominalTypeDeclaration(for node: NominalTypeDeclSyntaxNode, sourceFilePath: String) throws -> SwiftNominalTypeDeclaration { SwiftNominalTypeDeclaration( + sourceFilePath: sourceFilePath, moduleName: self.symbolTable.moduleName, parent: try parentTypeDecl(for: node), node: node @@ -136,7 +137,7 @@ class SwiftTypeLookupContext { while let parentDecl = node.ancestorDecl { switch parentDecl.as(DeclSyntaxEnum.self) { case .structDecl, .classDecl, .actorDecl, .enumDecl, .protocolDecl: - return (try typeDeclaration(for: parentDecl) as! SwiftNominalTypeDeclaration) + return (try typeDeclaration(for: parentDecl, sourceFilePath: "FIXME_NO_SOURCE_FILE.swift") as! SwiftNominalTypeDeclaration) // FIXME: need to get the source file of the parent default: node = parentDecl continue diff --git a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift index 66563fee..001d34fa 100644 --- a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift +++ b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift @@ -42,7 +42,7 @@ func assertOutput( let translator = Swift2JavaTranslator(config: config) translator.dependenciesClasses = Array(javaClassLookupTable.keys) - try! translator.analyze(file: "/fake/Fake.swiftinterface", text: input) + try! translator.analyze(path: "/fake/Fake.swiftinterface", text: input) let output: String var printer: CodePrinter = CodePrinter(mode: .accumulateAll) diff --git a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift index 82747ec9..79b51c19 100644 --- a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift +++ b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift @@ -42,7 +42,7 @@ final class FuncCallbackImportTests { let st = Swift2JavaTranslator(config: config) st.log.logLevel = .error - try st.analyze(file: "Fake.swift", text: Self.class_interfaceFile) + try st.analyze(path: "Fake.swift", text: Self.class_interfaceFile) let funcDecl = st.importedGlobalFuncs.first { $0.name == "callMe" }! @@ -131,7 +131,7 @@ final class FuncCallbackImportTests { config.swiftModule = "__FakeModule" let st = Swift2JavaTranslator(config: config) - try st.analyze(file: "Fake.swift", text: Self.class_interfaceFile) + try st.analyze(path: "Fake.swift", text: Self.class_interfaceFile) let funcDecl = st.importedGlobalFuncs.first { $0.name == "callMeMore" }! @@ -247,7 +247,7 @@ final class FuncCallbackImportTests { let st = Swift2JavaTranslator(config: config) st.log.logLevel = .error - try st.analyze(file: "Fake.swift", text: Self.class_interfaceFile) + try st.analyze(path: "Fake.swift", text: Self.class_interfaceFile) let funcDecl = st.importedGlobalFuncs.first { $0.name == "withBuffer" }! diff --git a/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift b/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift index ff19f4a2..b6ae6f6c 100644 --- a/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift +++ b/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift @@ -239,7 +239,7 @@ extension FunctionDescriptorTests { let st = Swift2JavaTranslator(config: config) st.log.logLevel = logLevel - try st.analyze(file: "/fake/Sample.swiftinterface", text: interfaceFile) + try st.analyze(path: "/fake/Sample.swiftinterface", text: interfaceFile) let funcDecl = st.importedGlobalFuncs.first { $0.name == methodIdentifier @@ -273,7 +273,7 @@ extension FunctionDescriptorTests { let st = Swift2JavaTranslator(config: config) st.log.logLevel = logLevel - try st.analyze(file: "/fake/Sample.swiftinterface", text: interfaceFile) + try st.analyze(path: "/fake/Sample.swiftinterface", text: interfaceFile) let generator = FFMSwift2JavaGenerator( config: config, diff --git a/Tests/JExtractSwiftTests/MethodImportTests.swift b/Tests/JExtractSwiftTests/MethodImportTests.swift index 938b5e7f..6ba93016 100644 --- a/Tests/JExtractSwiftTests/MethodImportTests.swift +++ b/Tests/JExtractSwiftTests/MethodImportTests.swift @@ -70,7 +70,7 @@ final class MethodImportTests { let st = Swift2JavaTranslator(config: config) st.log.logLevel = .error - try st.analyze(file: "Fake.swift", text: class_interfaceFile) + try st.analyze(path: "Fake.swift", text: class_interfaceFile) let generator = FFMSwift2JavaGenerator( config: config, @@ -110,7 +110,7 @@ final class MethodImportTests { let st = Swift2JavaTranslator(config: config) st.log.logLevel = .error - try st.analyze(file: "Fake.swift", text: class_interfaceFile) + try st.analyze(path: "Fake.swift", text: class_interfaceFile) let funcDecl = st.importedGlobalFuncs.first { $0.name == "globalTakeInt" @@ -152,7 +152,7 @@ final class MethodImportTests { let st = Swift2JavaTranslator(config: config) st.log.logLevel = .error - try st.analyze(file: "Fake.swift", text: class_interfaceFile) + try st.analyze(path: "Fake.swift", text: class_interfaceFile) let funcDecl = st.importedGlobalFuncs.first { $0.name == "globalTakeIntLongString" @@ -196,7 +196,7 @@ final class MethodImportTests { let st = Swift2JavaTranslator(config: config) st.log.logLevel = .error - try st.analyze(file: "Fake.swift", text: class_interfaceFile) + try st.analyze(path: "Fake.swift", text: class_interfaceFile) let funcDecl = st.importedGlobalFuncs.first { $0.name == "globalReturnClass" @@ -240,7 +240,7 @@ final class MethodImportTests { let st = Swift2JavaTranslator(config: config) st.log.logLevel = .error - try st.analyze(file: "Fake.swift", text: class_interfaceFile) + try st.analyze(path: "Fake.swift", text: class_interfaceFile) let funcDecl = st.importedGlobalFuncs.first { $0.name == "swapRawBufferPointer" @@ -287,7 +287,7 @@ final class MethodImportTests { let st = Swift2JavaTranslator(config: config) st.log.logLevel = .error - try st.analyze(file: "Fake.swift", text: class_interfaceFile) + try st.analyze(path: "Fake.swift", text: class_interfaceFile) let funcDecl: ImportedFunc = st.importedTypes["MySwiftClass"]!.methods.first { $0.name == "helloMemberFunction" @@ -330,7 +330,7 @@ final class MethodImportTests { let st = Swift2JavaTranslator(config: config) st.log.logLevel = .info - try st.analyze(file: "Fake.swift", text: class_interfaceFile) + try st.analyze(path: "Fake.swift", text: class_interfaceFile) let funcDecl: ImportedFunc = st.importedTypes["MySwiftClass"]!.methods.first { $0.name == "makeInt" @@ -373,7 +373,7 @@ final class MethodImportTests { let st = Swift2JavaTranslator(config: config) st.log.logLevel = .info - try st.analyze(file: "Fake.swift", text: class_interfaceFile) + try st.analyze(path: "Fake.swift", text: class_interfaceFile) let initDecl: ImportedFunc = st.importedTypes["MySwiftClass"]!.initializers.first { $0.name == "init" @@ -418,7 +418,7 @@ final class MethodImportTests { st.log.logLevel = .info - try st.analyze(file: "Fake.swift", text: class_interfaceFile) + try st.analyze(path: "Fake.swift", text: class_interfaceFile) let initDecl: ImportedFunc = st.importedTypes["MySwiftStruct"]!.initializers.first { $0.name == "init" diff --git a/Tests/JExtractSwiftTests/SwiftSymbolTableTests.swift b/Tests/JExtractSwiftTests/SwiftSymbolTableTests.swift index fdbf2d5f..b437454c 100644 --- a/Tests/JExtractSwiftTests/SwiftSymbolTableTests.swift +++ b/Tests/JExtractSwiftTests/SwiftSymbolTableTests.swift @@ -34,7 +34,10 @@ struct SwiftSymbolTableSuite { """ let symbolTable = SwiftSymbolTable.setup( moduleName: "MyModule", - [sourceFile1, sourceFile2], + [ + .init(syntax: sourceFile1, path: "Fake.swift"), + .init(syntax: sourceFile2, path: "Fake2.swift") + ], log: Logger(label: "swift-java", logLevel: .critical) ) From 499ea036df1edb4860c59931bd4f50c952d5eec5 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Fri, 24 Oct 2025 21:15:37 +0900 Subject: [PATCH 2/4] add tests to verify we generated the expected types --- .../MultipleTypesFromSingleFileTest.java | 36 +++++++++++++++++++ .../MultipleTypesFromSingleFileTest.java | 36 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MultipleTypesFromSingleFileTest.java create mode 100644 Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MultipleTypesFromSingleFileTest.java diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MultipleTypesFromSingleFileTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MultipleTypesFromSingleFileTest.java new file mode 100644 index 00000000..d3cd791c --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MultipleTypesFromSingleFileTest.java @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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.*; +import org.swift.swiftkit.ffm.*; + +import java.lang.foreign.Arena; +import java.util.Optional; +import java.util.OptionalInt; + +import static org.junit.jupiter.api.Assertions.*; + +public class MultipleTypesFromSingleFileTest { + + @Test + void bothTypesMustHaveBeenGenerated() { + try (var arena = AllocatingSwiftArena.ofConfined()) { + PublicTypeOne.init(arena); + PublicTypeTwo.init(arena); + } + } +} \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MultipleTypesFromSingleFileTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MultipleTypesFromSingleFileTest.java new file mode 100644 index 00000000..ae02b872 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MultipleTypesFromSingleFileTest.java @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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.ConfinedSwiftMemorySession; +import org.swift.swiftkit.core.SwiftArena; + +import java.lang.foreign.Arena; +import java.util.Optional; +import java.util.OptionalInt; + +import static org.junit.jupiter.api.Assertions.*; + +public class MultipleTypesFromSingleFileTest { + + @Test + void bothTypesMustHaveBeenGenerated() { + try (var arena = SwiftArena.ofConfined()) { + PublicTypeOne.init(arena); + PublicTypeTwo.init(arena); + } + } +} \ No newline at end of file From c1cf10102aecfed04eeec991ffc3c6ae582a8ae4 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Fri, 24 Oct 2025 22:11:57 +0900 Subject: [PATCH 3/4] avoid dead code warning --- Sources/SwiftJava/JavaKitVM/JavaVirtualMachine.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftJava/JavaKitVM/JavaVirtualMachine.swift b/Sources/SwiftJava/JavaKitVM/JavaVirtualMachine.swift index 7443039a..bb574c8a 100644 --- a/Sources/SwiftJava/JavaKitVM/JavaVirtualMachine.swift +++ b/Sources/SwiftJava/JavaKitVM/JavaVirtualMachine.swift @@ -190,8 +190,8 @@ extension JavaVirtualMachine { // If we failed to attach, report that. if let attachError = VMError(fromJNIError: attachResult) { + // throw attachError fatalError("JVM Error: \(attachError)") - throw attachError } JavaVirtualMachine.destroyTLS.set(jniEnv!) From 72ce01beaa8c675128aa0faed9fe5b379a7faf80 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Fri, 24 Oct 2025 23:00:38 +0900 Subject: [PATCH 4/4] try build stability workaround --- Samples/SwiftJavaExtractFFMSampleApp/ci-validate.sh | 2 ++ Samples/SwiftJavaExtractJNISampleApp/build.gradle | 3 ++- Samples/SwiftJavaExtractJNISampleApp/ci-validate.sh | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Samples/SwiftJavaExtractFFMSampleApp/ci-validate.sh b/Samples/SwiftJavaExtractFFMSampleApp/ci-validate.sh index c7a68d22..8758bbee 100755 --- a/Samples/SwiftJavaExtractFFMSampleApp/ci-validate.sh +++ b/Samples/SwiftJavaExtractFFMSampleApp/ci-validate.sh @@ -3,5 +3,7 @@ set -x set -e +swift build # as a workaround for building swift build from within gradle having issues on CI sometimes + ./gradlew run ./gradlew test \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/build.gradle b/Samples/SwiftJavaExtractJNISampleApp/build.gradle index b1aa8490..d92c96fb 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/build.gradle +++ b/Samples/SwiftJavaExtractJNISampleApp/build.gradle @@ -102,7 +102,8 @@ def jextract = tasks.register("jextract", Exec) { workingDir = layout.projectDirectory commandLine "swift" - args("build") // since Swift targets which need to be jextract-ed have the jextract build plugin, we just need to build + // TODO: -v for debugging build issues... + args("build", "-v") // since Swift targets which need to be jextract-ed have the jextract build plugin, we just need to build // If we wanted to execute a specific subcommand, we can like this: // args("run",/* // "swift-java", "jextract", diff --git a/Samples/SwiftJavaExtractJNISampleApp/ci-validate.sh b/Samples/SwiftJavaExtractJNISampleApp/ci-validate.sh index c7a68d22..8758bbee 100755 --- a/Samples/SwiftJavaExtractJNISampleApp/ci-validate.sh +++ b/Samples/SwiftJavaExtractJNISampleApp/ci-validate.sh @@ -3,5 +3,7 @@ set -x set -e +swift build # as a workaround for building swift build from within gradle having issues on CI sometimes + ./gradlew run ./gradlew test \ No newline at end of file