Skip to content

Commit 0cb7a81

Browse files
BridgeJS: Fix multifile declaration resolution order issue
Close #487
1 parent 68bff4d commit 0cb7a81

15 files changed

+969
-10
lines changed

Plugins/BridgeJS/Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ let package = Package(
5454
"BridgeJSLink",
5555
"TS2Skeleton",
5656
],
57-
exclude: ["__Snapshots__", "Inputs"]
57+
exclude: ["__Snapshots__", "Inputs", "MultifileInputs"]
5858
),
5959
]
6060
)

Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public class ExportSwift {
2828
private var exportedProtocols: [ExportedProtocol] = []
2929
private var exportedProtocolNameByKey: [String: String] = [:]
3030
private var typeDeclResolver: TypeDeclResolver = TypeDeclResolver()
31+
private var sourceFiles: [(sourceFile: SourceFileSyntax, inputFilePath: String)] = []
3132

3233
public init(progress: ProgressReporting, moduleName: String, exposeToGlobal: Bool) {
3334
self.progress = progress
@@ -41,16 +42,9 @@ public class ExportSwift {
4142
/// - sourceFile: The parsed Swift source file to process
4243
/// - inputFilePath: The file path for error reporting
4344
public func addSourceFile(_ sourceFile: SourceFileSyntax, _ inputFilePath: String) throws {
44-
progress.print("Processing \(inputFilePath)")
45+
// First, register type declarations before walking for exposed APIs
4546
typeDeclResolver.addSourceFile(sourceFile)
46-
47-
let errors = try parseSingleFile(sourceFile)
48-
if errors.count > 0 {
49-
throw BridgeJSCoreError(
50-
errors.map { $0.formattedDescription(fileName: inputFilePath) }
51-
.joined(separator: "\n")
52-
)
53-
}
47+
sourceFiles.append((sourceFile, inputFilePath))
5448
}
5549

5650
/// Finalizes the export process and generates the bridge code
@@ -60,6 +54,27 @@ public class ExportSwift {
6054
/// - Returns: A tuple containing the generated Swift code and a skeleton
6155
/// describing the exported APIs
6256
public func finalize() throws -> (outputSwift: String, outputSkeleton: ExportedSkeleton)? {
57+
// Walk through each source file and collect exported APIs
58+
var perSourceErrors: [(inputFilePath: String, errors: [DiagnosticError])] = []
59+
for (sourceFile, inputFilePath) in sourceFiles {
60+
progress.print("Processing \(inputFilePath)")
61+
let errors = try parseSingleFile(sourceFile)
62+
if errors.count > 0 {
63+
perSourceErrors.append((inputFilePath: inputFilePath, errors: errors))
64+
}
65+
}
66+
67+
if !perSourceErrors.isEmpty {
68+
// Aggregate and throw all errors
69+
var allErrors: [String] = []
70+
for (inputFilePath, errors) in perSourceErrors {
71+
for error in errors {
72+
allErrors.append(error.formattedDescription(fileName: inputFilePath))
73+
}
74+
}
75+
throw BridgeJSCoreError(allErrors.joined(separator: "\n"))
76+
}
77+
6378
guard let outputSwift = try renderSwiftGlue() else {
6479
return nil
6580
}

Plugins/BridgeJS/Tests/BridgeJSToolTests/ExportSwiftTests.swift

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ import Testing
3939
"Inputs"
4040
)
4141

42+
static let multifileInputsDirectory = URL(fileURLWithPath: #filePath).deletingLastPathComponent().appendingPathComponent(
43+
"MultifileInputs"
44+
)
45+
4246
static func collectInputs() -> [String] {
4347
let fileManager = FileManager.default
4448
let inputs = try! fileManager.contentsOfDirectory(atPath: Self.inputsDirectory.path)
@@ -69,4 +73,73 @@ import Testing
6973
let name = url.deletingPathExtension().lastPathComponent
7074
try snapshot(swiftAPI: swiftAPI, name: name + ".Global")
7175
}
76+
77+
@Test
78+
func snapshotCrossFileTypeResolution() throws {
79+
// Test that types defined in one file can be referenced from another file
80+
// This tests the fix for cross-file type resolution in BridgeJS
81+
let swiftAPI = ExportSwift(progress: .silent, moduleName: "TestModule", exposeToGlobal: false)
82+
83+
// Add ClassB first, then ClassA (which references ClassB)
84+
let classBURL = Self.multifileInputsDirectory.appendingPathComponent("CrossFileClassB.swift")
85+
let classBSourceFile = Parser.parse(source: try String(contentsOf: classBURL, encoding: .utf8))
86+
try swiftAPI.addSourceFile(classBSourceFile, "CrossFileClassB.swift")
87+
88+
let classAURL = Self.multifileInputsDirectory.appendingPathComponent("CrossFileClassA.swift")
89+
let classASourceFile = Parser.parse(source: try String(contentsOf: classAURL, encoding: .utf8))
90+
try swiftAPI.addSourceFile(classASourceFile, "CrossFileClassA.swift")
91+
92+
try snapshot(swiftAPI: swiftAPI, name: "CrossFileTypeResolution")
93+
}
94+
95+
@Test
96+
func snapshotCrossFileTypeResolutionReverseOrder() throws {
97+
// Test that types can be resolved regardless of the order files are added
98+
// Add ClassA first (which references ClassB), then ClassB
99+
let swiftAPI = ExportSwift(progress: .silent, moduleName: "TestModule", exposeToGlobal: false)
100+
101+
let classAURL = Self.multifileInputsDirectory.appendingPathComponent("CrossFileClassA.swift")
102+
let classASourceFile = Parser.parse(source: try String(contentsOf: classAURL, encoding: .utf8))
103+
try swiftAPI.addSourceFile(classASourceFile, "CrossFileClassA.swift")
104+
105+
let classBURL = Self.multifileInputsDirectory.appendingPathComponent("CrossFileClassB.swift")
106+
let classBSourceFile = Parser.parse(source: try String(contentsOf: classBURL, encoding: .utf8))
107+
try swiftAPI.addSourceFile(classBSourceFile, "CrossFileClassB.swift")
108+
109+
try snapshot(swiftAPI: swiftAPI, name: "CrossFileTypeResolution.ReverseOrder")
110+
}
111+
112+
@Test
113+
func snapshotCrossFileFunctionTypes() throws {
114+
// Test that functions and methods can use cross-file types as parameters and return types
115+
let swiftAPI = ExportSwift(progress: .silent, moduleName: "TestModule", exposeToGlobal: false)
116+
117+
// Add FunctionB first, then FunctionA (which references FunctionB in methods and functions)
118+
let functionBURL = Self.multifileInputsDirectory.appendingPathComponent("CrossFileFunctionB.swift")
119+
let functionBSourceFile = Parser.parse(source: try String(contentsOf: functionBURL, encoding: .utf8))
120+
try swiftAPI.addSourceFile(functionBSourceFile, "CrossFileFunctionB.swift")
121+
122+
let functionAURL = Self.multifileInputsDirectory.appendingPathComponent("CrossFileFunctionA.swift")
123+
let functionASourceFile = Parser.parse(source: try String(contentsOf: functionAURL, encoding: .utf8))
124+
try swiftAPI.addSourceFile(functionASourceFile, "CrossFileFunctionA.swift")
125+
126+
try snapshot(swiftAPI: swiftAPI, name: "CrossFileFunctionTypes")
127+
}
128+
129+
@Test
130+
func snapshotCrossFileFunctionTypesReverseOrder() throws {
131+
// Test that function types can be resolved regardless of the order files are added
132+
let swiftAPI = ExportSwift(progress: .silent, moduleName: "TestModule", exposeToGlobal: false)
133+
134+
// Add FunctionA first (which references FunctionB), then FunctionB
135+
let functionAURL = Self.multifileInputsDirectory.appendingPathComponent("CrossFileFunctionA.swift")
136+
let functionASourceFile = Parser.parse(source: try String(contentsOf: functionAURL, encoding: .utf8))
137+
try swiftAPI.addSourceFile(functionASourceFile, "CrossFileFunctionA.swift")
138+
139+
let functionBURL = Self.multifileInputsDirectory.appendingPathComponent("CrossFileFunctionB.swift")
140+
let functionBSourceFile = Parser.parse(source: try String(contentsOf: functionBURL, encoding: .utf8))
141+
try swiftAPI.addSourceFile(functionBSourceFile, "CrossFileFunctionB.swift")
142+
143+
try snapshot(swiftAPI: swiftAPI, name: "CrossFileFunctionTypes.ReverseOrder")
144+
}
72145
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
@JS class ClassA {
2+
@JS var linkedB: ClassB?
3+
}
4+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
@JS class ClassB {
2+
@JS init() {}
3+
}
4+
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
@JS class FunctionA {
2+
@JS init() {}
3+
4+
// Method that takes a cross-file type as parameter
5+
@JS func processB(b: FunctionB) -> String {
6+
return "Processed \(b.value)"
7+
}
8+
9+
// Method that returns a cross-file type
10+
@JS func createB(value: String) -> FunctionB {
11+
return FunctionB(value: value)
12+
}
13+
}
14+
15+
// Standalone function that uses cross-file types
16+
@JS func standaloneFunction(b: FunctionB) -> FunctionB {
17+
return b
18+
}
19+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
@JS class FunctionB {
2+
@JS var value: String
3+
4+
@JS init(value: String) {
5+
self.value = value
6+
}
7+
}
8+
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
{
2+
"classes" : [
3+
{
4+
"constructor" : {
5+
"abiName" : "bjs_FunctionA_init",
6+
"effects" : {
7+
"isAsync" : false,
8+
"isStatic" : false,
9+
"isThrows" : false
10+
},
11+
"parameters" : [
12+
13+
]
14+
},
15+
"methods" : [
16+
{
17+
"abiName" : "bjs_FunctionA_processB",
18+
"effects" : {
19+
"isAsync" : false,
20+
"isStatic" : false,
21+
"isThrows" : false
22+
},
23+
"name" : "processB",
24+
"parameters" : [
25+
{
26+
"label" : "b",
27+
"name" : "b",
28+
"type" : {
29+
"swiftHeapObject" : {
30+
"_0" : "FunctionB"
31+
}
32+
}
33+
}
34+
],
35+
"returnType" : {
36+
"string" : {
37+
38+
}
39+
}
40+
},
41+
{
42+
"abiName" : "bjs_FunctionA_createB",
43+
"effects" : {
44+
"isAsync" : false,
45+
"isStatic" : false,
46+
"isThrows" : false
47+
},
48+
"name" : "createB",
49+
"parameters" : [
50+
{
51+
"label" : "value",
52+
"name" : "value",
53+
"type" : {
54+
"string" : {
55+
56+
}
57+
}
58+
}
59+
],
60+
"returnType" : {
61+
"swiftHeapObject" : {
62+
"_0" : "FunctionB"
63+
}
64+
}
65+
}
66+
],
67+
"name" : "FunctionA",
68+
"properties" : [
69+
70+
],
71+
"swiftCallName" : "FunctionA"
72+
},
73+
{
74+
"constructor" : {
75+
"abiName" : "bjs_FunctionB_init",
76+
"effects" : {
77+
"isAsync" : false,
78+
"isStatic" : false,
79+
"isThrows" : false
80+
},
81+
"parameters" : [
82+
{
83+
"label" : "value",
84+
"name" : "value",
85+
"type" : {
86+
"string" : {
87+
88+
}
89+
}
90+
}
91+
]
92+
},
93+
"methods" : [
94+
95+
],
96+
"name" : "FunctionB",
97+
"properties" : [
98+
{
99+
"isReadonly" : false,
100+
"isStatic" : false,
101+
"name" : "value",
102+
"type" : {
103+
"string" : {
104+
105+
}
106+
}
107+
}
108+
],
109+
"swiftCallName" : "FunctionB"
110+
}
111+
],
112+
"enums" : [
113+
114+
],
115+
"exposeToGlobal" : false,
116+
"functions" : [
117+
{
118+
"abiName" : "bjs_standaloneFunction",
119+
"effects" : {
120+
"isAsync" : false,
121+
"isStatic" : false,
122+
"isThrows" : false
123+
},
124+
"name" : "standaloneFunction",
125+
"parameters" : [
126+
{
127+
"label" : "b",
128+
"name" : "b",
129+
"type" : {
130+
"swiftHeapObject" : {
131+
"_0" : "FunctionB"
132+
}
133+
}
134+
}
135+
],
136+
"returnType" : {
137+
"swiftHeapObject" : {
138+
"_0" : "FunctionB"
139+
}
140+
}
141+
}
142+
],
143+
"moduleName" : "TestModule",
144+
"protocols" : [
145+
146+
],
147+
"structs" : [
148+
149+
]
150+
}

0 commit comments

Comments
 (0)