From 36e74f60d8517e08269c1bf7c7e4c61a87a71f20 Mon Sep 17 00:00:00 2001
From: Rintaro Ishizaki <rishizaki@apple.com>
Date: Tue, 15 Apr 2025 23:41:01 +0900
Subject: [PATCH] [jextract] Prepare all inputs before analyze()

Register all the files to `Swift2JavaTranslator` (and
`NominalTypeResolution`) and `analye()` all the files at once.

`Swift2JavaVisitor.visit(_: ExtentionDeclSyntax)` requires all the
nominal types are prepared in the `nominalResolution`. Otherwise it's
just ignored.

Also previously, writing files happend for each file iteration but
actually, it is not a per-source file operation. Instead, it only writes
the current state of the translator.
---
 Sources/JExtractSwift/Swift2Java.swift        | 23 ++++++---
 .../JExtractSwift/Swift2JavaTranslator.swift  | 50 +++++++++++--------
 .../Asserts/LoweringAssertions.swift          |  4 +-
 3 files changed, 46 insertions(+), 31 deletions(-)

diff --git a/Sources/JExtractSwift/Swift2Java.swift b/Sources/JExtractSwift/Swift2Java.swift
index a46d1ab4..5eaab18a 100644
--- a/Sources/JExtractSwift/Swift2Java.swift
+++ b/Sources/JExtractSwift/Swift2Java.swift
@@ -74,16 +74,23 @@ public struct SwiftToJava: ParsableCommand {
       }
     }
 
-    for file in allFiles where canExtract(from: file) {
-      translator.log.debug("Importing module '\(swiftModule)', file: \(file)")
-
-      try translator.analyze(file: file.path)
-      try translator.writeExportedJavaSources(outputDirectory: outputDirectoryJava)
-      try translator.writeSwiftThunkSources(outputDirectory: outputDirectorySwift)
-
-      log.debug("[swift-java] Imported interface file: \(file.path)")
+    // Register files to the translator.
+    for file in allFiles {
+      guard canExtract(from: file) else {
+        continue
+      }
+      guard let data = fileManager.contents(atPath: file.path) else {
+        continue
+      }
+      guard let text = String(data:data, encoding: .utf8) else {
+        continue
+      }
+      translator.add(filePath: file.path, text: text)
     }
 
+    try translator.analyze()
+    try translator.writeSwiftThunkSources(outputDirectory: outputDirectorySwift)
+    try translator.writeExportedJavaSources(outputDirectory: outputDirectoryJava)
     try translator.writeExportedJavaModule(outputDirectory: outputDirectoryJava)
     print("[swift-java] Generated Java sources (\(packageName)) in: \(outputDirectoryJava)/")
     print("[swift-java] Imported Swift module '\(swiftModule)': " + "done.".green)
diff --git a/Sources/JExtractSwift/Swift2JavaTranslator.swift b/Sources/JExtractSwift/Swift2JavaTranslator.swift
index 6577a37c..446dd4c2 100644
--- a/Sources/JExtractSwift/Swift2JavaTranslator.swift
+++ b/Sources/JExtractSwift/Swift2JavaTranslator.swift
@@ -24,6 +24,15 @@ public final class Swift2JavaTranslator {
 
   package var log = Logger(label: "translator", logLevel: .info)
 
+  // ==== Input
+
+  struct Input {
+    let filePath: String
+    let syntax: Syntax
+  }
+
+  var inputs: [Input] = []
+
   // ==== Output configuration
   let javaPackage: String
 
@@ -39,7 +48,7 @@ public final class Swift2JavaTranslator {
   /// type representation.
   package var importedTypes: [String: ImportedNominalType] = [:]
 
-  public var swiftStdlibTypes: SwiftStandardLibraryTypes
+  package var swiftStdlibTypes: SwiftStandardLibraryTypes
 
   let symbolTable: SwiftSymbolTable
   let nominalResolution: NominalTypeResolution = NominalTypeResolution()
@@ -78,26 +87,24 @@ extension Swift2JavaTranslator {
   /// a checked truncation operation at the Java/Swift board.
   var javaPrimitiveForSwiftInt: JavaType { .long }
 
-  public func analyze(
+  package func add(filePath: String, text: String) {
+    log.trace("Adding: \(filePath)")
+    let sourceFileSyntax = Parser.parse(source: text)
+    self.nominalResolution.addSourceFile(sourceFileSyntax)
+    self.inputs.append(Input(filePath: filePath, syntax: Syntax(sourceFileSyntax)))
+  }
+
+  /// Convenient method for analyzing single file.
+  package func analyze(
     file: String,
-    text: String? = nil
+    text: String
   ) throws {
-    guard text != nil || FileManager.default.fileExists(atPath: file) else {
-      throw Swift2JavaTranslatorError(message: "Missing input file: \(file)")
-    }
-
-    log.trace("Analyze: \(file)")
-    let text = try text ?? String(contentsOfFile: file)
-
-    try analyzeSwiftInterface(interfaceFilePath: file, text: text)
-
-    log.debug("Done processing: \(file)")
+    self.add(filePath: file, text: text)
+    try self.analyze()
   }
 
-  package func analyzeSwiftInterface(interfaceFilePath: String, text: String) throws {
-    let sourceFileSyntax = Parser.parse(source: text)
-
-    addSourceFile(sourceFileSyntax)
+  /// Analyze registered inputs.
+  func analyze() throws {
     prepareForTranslation()
 
     let visitor = Swift2JavaVisitor(
@@ -105,16 +112,17 @@ extension Swift2JavaTranslator {
       targetJavaPackage: self.javaPackage,
       translator: self
     )
-    visitor.walk(sourceFileSyntax)
-  }
 
-  package func addSourceFile(_ sourceFile: SourceFileSyntax) {
-    nominalResolution.addSourceFile(sourceFile)
+    for input in self.inputs {
+      log.trace("Analyzing \(input.filePath)")
+      visitor.walk(input.syntax)
+    }
   }
 
   package func prepareForTranslation() {
     nominalResolution.bindExtensions()
 
+    // Prepare symbol table for nominal type names.
     for (_, node) in nominalResolution.topLevelNominalTypes {
       symbolTable.parsedModule.addNominalTypeDeclaration(node, parent: nil)
     }
diff --git a/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift b/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift
index 6f7a5e83..c37325cb 100644
--- a/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift
+++ b/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift
@@ -22,7 +22,7 @@ func assertLoweredFunction(
   _ inputDecl: DeclSyntax,
   javaPackage: String = "org.swift.mypackage",
   swiftModuleName: String = "MyModule",
-  sourceFile: SourceFileSyntax? = nil,
+  sourceFile: String? = nil,
   enclosingType: TypeSyntax? = nil,
   expectedCDecl: DeclSyntax,
   expectedCFunction: String,
@@ -37,7 +37,7 @@ func assertLoweredFunction(
   )
 
   if let sourceFile {
-    translator.addSourceFile(sourceFile)
+    translator.add(filePath: "Fake.swift", text: sourceFile)
   }
 
   translator.prepareForTranslation()