Skip to content

Commit 5f536c8

Browse files
authored
Merge pull request #488 from swiftwasm/yt/fix-multifile-decl-resolution
BridgeJS: Fix multifile declaration resolution order issue
2 parents b050829 + b8311c4 commit 5f536c8

15 files changed

+966
-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: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ import Testing
3939
"Inputs"
4040
)
4141

42+
static let multifileInputsDirectory = URL(fileURLWithPath: #filePath).deletingLastPathComponent()
43+
.appendingPathComponent(
44+
"MultifileInputs"
45+
)
46+
4247
static func collectInputs() -> [String] {
4348
let fileManager = FileManager.default
4449
let inputs = try! fileManager.contentsOfDirectory(atPath: Self.inputsDirectory.path)
@@ -69,4 +74,73 @@ import Testing
6974
let name = url.deletingPathExtension().lastPathComponent
7075
try snapshot(swiftAPI: swiftAPI, name: name + ".Global")
7176
}
77+
78+
@Test
79+
func snapshotCrossFileTypeResolution() throws {
80+
// Test that types defined in one file can be referenced from another file
81+
// This tests the fix for cross-file type resolution in BridgeJS
82+
let swiftAPI = ExportSwift(progress: .silent, moduleName: "TestModule", exposeToGlobal: false)
83+
84+
// Add ClassB first, then ClassA (which references ClassB)
85+
let classBURL = Self.multifileInputsDirectory.appendingPathComponent("CrossFileClassB.swift")
86+
let classBSourceFile = Parser.parse(source: try String(contentsOf: classBURL, encoding: .utf8))
87+
try swiftAPI.addSourceFile(classBSourceFile, "CrossFileClassB.swift")
88+
89+
let classAURL = Self.multifileInputsDirectory.appendingPathComponent("CrossFileClassA.swift")
90+
let classASourceFile = Parser.parse(source: try String(contentsOf: classAURL, encoding: .utf8))
91+
try swiftAPI.addSourceFile(classASourceFile, "CrossFileClassA.swift")
92+
93+
try snapshot(swiftAPI: swiftAPI, name: "CrossFileTypeResolution")
94+
}
95+
96+
@Test
97+
func snapshotCrossFileTypeResolutionReverseOrder() throws {
98+
// Test that types can be resolved regardless of the order files are added
99+
// Add ClassA first (which references ClassB), then ClassB
100+
let swiftAPI = ExportSwift(progress: .silent, moduleName: "TestModule", exposeToGlobal: false)
101+
102+
let classAURL = Self.multifileInputsDirectory.appendingPathComponent("CrossFileClassA.swift")
103+
let classASourceFile = Parser.parse(source: try String(contentsOf: classAURL, encoding: .utf8))
104+
try swiftAPI.addSourceFile(classASourceFile, "CrossFileClassA.swift")
105+
106+
let classBURL = Self.multifileInputsDirectory.appendingPathComponent("CrossFileClassB.swift")
107+
let classBSourceFile = Parser.parse(source: try String(contentsOf: classBURL, encoding: .utf8))
108+
try swiftAPI.addSourceFile(classBSourceFile, "CrossFileClassB.swift")
109+
110+
try snapshot(swiftAPI: swiftAPI, name: "CrossFileTypeResolution.ReverseOrder")
111+
}
112+
113+
@Test
114+
func snapshotCrossFileFunctionTypes() throws {
115+
// Test that functions and methods can use cross-file types as parameters and return types
116+
let swiftAPI = ExportSwift(progress: .silent, moduleName: "TestModule", exposeToGlobal: false)
117+
118+
// Add FunctionB first, then FunctionA (which references FunctionB in methods and functions)
119+
let functionBURL = Self.multifileInputsDirectory.appendingPathComponent("CrossFileFunctionB.swift")
120+
let functionBSourceFile = Parser.parse(source: try String(contentsOf: functionBURL, encoding: .utf8))
121+
try swiftAPI.addSourceFile(functionBSourceFile, "CrossFileFunctionB.swift")
122+
123+
let functionAURL = Self.multifileInputsDirectory.appendingPathComponent("CrossFileFunctionA.swift")
124+
let functionASourceFile = Parser.parse(source: try String(contentsOf: functionAURL, encoding: .utf8))
125+
try swiftAPI.addSourceFile(functionASourceFile, "CrossFileFunctionA.swift")
126+
127+
try snapshot(swiftAPI: swiftAPI, name: "CrossFileFunctionTypes")
128+
}
129+
130+
@Test
131+
func snapshotCrossFileFunctionTypesReverseOrder() throws {
132+
// Test that function types can be resolved regardless of the order files are added
133+
let swiftAPI = ExportSwift(progress: .silent, moduleName: "TestModule", exposeToGlobal: false)
134+
135+
// Add FunctionA first (which references FunctionB), then FunctionB
136+
let functionAURL = Self.multifileInputsDirectory.appendingPathComponent("CrossFileFunctionA.swift")
137+
let functionASourceFile = Parser.parse(source: try String(contentsOf: functionAURL, encoding: .utf8))
138+
try swiftAPI.addSourceFile(functionASourceFile, "CrossFileFunctionA.swift")
139+
140+
let functionBURL = Self.multifileInputsDirectory.appendingPathComponent("CrossFileFunctionB.swift")
141+
let functionBSourceFile = Parser.parse(source: try String(contentsOf: functionBURL, encoding: .utf8))
142+
try swiftAPI.addSourceFile(functionBSourceFile, "CrossFileFunctionB.swift")
143+
144+
try snapshot(swiftAPI: swiftAPI, name: "CrossFileFunctionTypes.ReverseOrder")
145+
}
72146
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@JS class ClassA {
2+
@JS var linkedB: ClassB?
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@JS class ClassB {
2+
@JS init() {}
3+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
@JS class FunctionB {
2+
@JS var value: String
3+
4+
@JS init(value: String) {
5+
self.value = value
6+
}
7+
}
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)