Skip to content

Commit 0872dbd

Browse files
committed
BridgeJS: Add default parameters to struct and enable struct as default parameter
1 parent 9b2a6b1 commit 0872dbd

File tree

14 files changed

+876
-194
lines changed

14 files changed

+876
-194
lines changed

Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Lines changed: 53 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -316,50 +316,77 @@ public class ExportSwift {
316316
return nil
317317
}
318318

319-
let className = calledExpr.baseName.text
320-
let expectedClassName: String?
319+
let typeName = calledExpr.baseName.text
320+
321+
let isStructType: Bool
322+
let expectedTypeName: String?
321323
switch type {
322-
case .swiftHeapObject(let name):
323-
expectedClassName = name.split(separator: ".").last.map(String.init)
324-
case .optional(.swiftHeapObject(let name)):
325-
expectedClassName = name.split(separator: ".").last.map(String.init)
324+
case .swiftStruct(let name), .optional(.swiftStruct(let name)):
325+
isStructType = true
326+
expectedTypeName = name.split(separator: ".").last.map(String.init)
327+
case .swiftHeapObject(let name), .optional(.swiftHeapObject(let name)):
328+
isStructType = false
329+
expectedTypeName = name.split(separator: ".").last.map(String.init)
326330
default:
327331
diagnose(
328332
node: funcCall,
329-
message: "Constructor calls are only supported for class types",
330-
hint: "Parameter type should be a Swift class"
333+
message: "Constructor calls are only supported for class and struct types",
334+
hint: "Parameter type should be a Swift class or struct"
331335
)
332336
return nil
333337
}
334338

335-
guard let expectedClassName = expectedClassName, className == expectedClassName else {
339+
guard let expectedTypeName = expectedTypeName, typeName == expectedTypeName else {
336340
diagnose(
337341
node: funcCall,
338-
message: "Constructor class name '\(className)' doesn't match parameter type",
342+
message: "Constructor type name '\(typeName)' doesn't match parameter type",
339343
hint: "Ensure the constructor matches the parameter type"
340344
)
341345
return nil
342346
}
343347

344-
if funcCall.arguments.isEmpty {
345-
return .object(className)
346-
}
347-
348-
var constructorArgs: [DefaultValue] = []
349-
for argument in funcCall.arguments {
350-
guard let argValue = extractLiteralValue(from: argument.expression) else {
351-
diagnose(
352-
node: argument.expression,
353-
message: "Constructor argument must be a literal value",
354-
hint: "Use simple literals like \"text\", 42, true, false in constructor arguments"
355-
)
356-
return nil
348+
if isStructType {
349+
// For structs, extract field name/value pairs
350+
var fields: [DefaultValueField] = []
351+
for argument in funcCall.arguments {
352+
guard let fieldName = argument.label?.text else {
353+
diagnose(
354+
node: argument,
355+
message: "Struct initializer arguments must have labels",
356+
hint: "Use labeled arguments like MyStruct(x: 1, y: 2)"
357+
)
358+
return nil
359+
}
360+
guard let fieldValue = extractLiteralValue(from: argument.expression) else {
361+
diagnose(
362+
node: argument.expression,
363+
message: "Struct field value must be a literal",
364+
hint: "Use simple literals like \"text\", 42, true, false in struct fields"
365+
)
366+
return nil
367+
}
368+
fields.append(DefaultValueField(name: fieldName, value: fieldValue))
369+
}
370+
return .structLiteral(typeName, fields)
371+
} else {
372+
if funcCall.arguments.isEmpty {
373+
return .object(typeName)
357374
}
358375

359-
constructorArgs.append(argValue)
376+
var constructorArgs: [DefaultValue] = []
377+
for argument in funcCall.arguments {
378+
guard let argValue = extractLiteralValue(from: argument.expression) else {
379+
diagnose(
380+
node: argument.expression,
381+
message: "Constructor argument must be a literal value",
382+
hint: "Use simple literals like \"text\", 42, true, false in constructor arguments"
383+
)
384+
return nil
385+
}
386+
constructorArgs.append(argValue)
387+
}
388+
return .objectWithArguments(typeName, constructorArgs)
360389
}
361-
362-
return .objectWithArguments(className, constructorArgs)
363390
}
364391

365392
/// Extracts a literal value from an expression with optional type checking

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 93 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1254,16 +1254,6 @@ struct BridgeJSLink {
12541254
]
12551255
}
12561256

1257-
func generateParameterList(parameters: [Parameter]) -> String {
1258-
parameters.map { param in
1259-
if let defaultValue = param.defaultValue {
1260-
let defaultJs = DefaultValueGenerator().generate(defaultValue, format: .javascript)
1261-
return "\(param.name) = \(defaultJs)"
1262-
}
1263-
return param.name
1264-
}.joined(separator: ", ")
1265-
}
1266-
12671257
func renderFunction(
12681258
name: String,
12691259
parameters: [Parameter],
@@ -1272,7 +1262,7 @@ struct BridgeJSLink {
12721262
) -> [String] {
12731263
let printer = CodeFragmentPrinter()
12741264

1275-
let parameterList = generateParameterList(parameters: parameters)
1265+
let parameterList = DefaultValueUtils.formatParameterList(parameters)
12761266

12771267
printer.write(
12781268
"\(declarationPrefixKeyword.map { "\($0) "} ?? "")\(name)(\(parameterList)) {"
@@ -1350,76 +1340,10 @@ struct BridgeJSLink {
13501340

13511341
/// Helper method to append JSDoc comments for parameters with default values
13521342
private func appendJSDocIfNeeded(for parameters: [Parameter], to lines: inout [String]) {
1353-
let jsDocLines = DefaultValueGenerator().generateJSDoc(for: parameters)
1343+
let jsDocLines = DefaultValueUtils.formatJSDoc(for: parameters)
13541344
lines.append(contentsOf: jsDocLines)
13551345
}
13561346

1357-
/// Helper struct for generating default value representations
1358-
private struct DefaultValueGenerator {
1359-
enum OutputFormat {
1360-
case javascript
1361-
case typescript
1362-
}
1363-
1364-
/// Generates default value representation for JavaScript or TypeScript
1365-
func generate(_ defaultValue: DefaultValue, format: OutputFormat) -> String {
1366-
switch defaultValue {
1367-
case .string(let value):
1368-
let escapedValue =
1369-
format == .javascript
1370-
? escapeForJavaScript(value)
1371-
: value // TypeScript doesn't need escape in doc comments
1372-
return "\"\(escapedValue)\""
1373-
case .int(let value):
1374-
return "\(value)"
1375-
case .float(let value):
1376-
return "\(value)"
1377-
case .double(let value):
1378-
return "\(value)"
1379-
case .bool(let value):
1380-
return value ? "true" : "false"
1381-
case .null:
1382-
return "null"
1383-
case .enumCase(let enumName, let caseName):
1384-
let simpleName = enumName.components(separatedBy: ".").last ?? enumName
1385-
let jsEnumName = format == .javascript ? "\(simpleName)\(ExportedEnum.valuesSuffix)" : simpleName
1386-
return "\(jsEnumName).\(caseName.capitalizedFirstLetter)"
1387-
case .object(let className):
1388-
return "new \(className)()"
1389-
case .objectWithArguments(let className, let args):
1390-
let argStrings = args.map { arg in
1391-
generate(arg, format: format)
1392-
}
1393-
return "new \(className)(\(argStrings.joined(separator: ", ")))"
1394-
}
1395-
}
1396-
1397-
private func escapeForJavaScript(_ string: String) -> String {
1398-
return
1399-
string
1400-
.replacingOccurrences(of: "\\", with: "\\\\")
1401-
.replacingOccurrences(of: "\"", with: "\\\"")
1402-
}
1403-
1404-
/// Generates JSDoc comment lines for parameters with default values
1405-
func generateJSDoc(for parameters: [Parameter]) -> [String] {
1406-
let paramsWithDefaults = parameters.filter { $0.hasDefault }
1407-
guard !paramsWithDefaults.isEmpty else {
1408-
return []
1409-
}
1410-
1411-
var jsDocLines: [String] = ["/**"]
1412-
for param in paramsWithDefaults {
1413-
if let defaultValue = param.defaultValue {
1414-
let defaultDoc = generate(defaultValue, format: .typescript)
1415-
jsDocLines.append(" * @param \(param.name) - Optional parameter (default: \(defaultDoc))")
1416-
}
1417-
}
1418-
jsDocLines.append(" */")
1419-
return jsDocLines
1420-
}
1421-
}
1422-
14231347
func renderExportedStruct(
14241348
_ structDefinition: ExportedStruct
14251349
) throws -> (js: [String], dtsType: [String], dtsExportEntry: [String]) {
@@ -1437,6 +1361,8 @@ struct BridgeJSLink {
14371361
dtsTypePrinter.write("\(property.name): \(tsType);")
14381362
}
14391363
for method in structDefinition.methods where !method.effects.isStatic {
1364+
let jsDocLines = DefaultValueUtils.formatJSDoc(for: method.parameters)
1365+
dtsTypePrinter.write(lines: jsDocLines)
14401366
let signature = renderTSSignature(
14411367
parameters: method.parameters,
14421368
returnType: method.returnType,
@@ -1466,7 +1392,7 @@ struct BridgeJSLink {
14661392
)
14671393

14681394
let constructorPrinter = CodeFragmentPrinter()
1469-
let paramList = thunkBuilder.generateParameterList(parameters: constructor.parameters)
1395+
let paramList = DefaultValueUtils.formatParameterList(constructor.parameters)
14701396
constructorPrinter.write("init: function(\(paramList)) {")
14711397
constructorPrinter.indent {
14721398
constructorPrinter.write(contentsOf: thunkBuilder.body)
@@ -1499,6 +1425,8 @@ struct BridgeJSLink {
14991425
dtsExportEntryPrinter.write("\(structName): {")
15001426
dtsExportEntryPrinter.indent {
15011427
if let constructor = structDefinition.constructor {
1428+
let jsDocLines = DefaultValueUtils.formatJSDoc(for: constructor.parameters)
1429+
dtsExportEntryPrinter.write(lines: jsDocLines)
15021430
dtsExportEntryPrinter.write(
15031431
"init\(renderTSSignature(parameters: constructor.parameters, returnType: .swiftStruct(structDefinition.swiftCallName), effects: constructor.effects));"
15041432
)
@@ -1508,6 +1436,8 @@ struct BridgeJSLink {
15081436
dtsExportEntryPrinter.write("\(readonly)\(property.name): \(resolveTypeScriptType(property.type));")
15091437
}
15101438
for method in staticMethods {
1439+
let jsDocLines = DefaultValueUtils.formatJSDoc(for: method.parameters)
1440+
dtsExportEntryPrinter.write(lines: jsDocLines)
15111441
dtsExportEntryPrinter.write(
15121442
"\(method.name)\(renderTSSignature(parameters: method.parameters, returnType: method.returnType, effects: method.effects));"
15131443
)
@@ -1930,7 +1860,7 @@ extension BridgeJSLink {
19301860
try thunkBuilder.lowerParameter(param: param)
19311861
}
19321862

1933-
let constructorParamList = thunkBuilder.generateParameterList(parameters: constructor.parameters)
1863+
let constructorParamList = DefaultValueUtils.formatParameterList(constructor.parameters)
19341864

19351865
jsPrinter.indent {
19361866
jsPrinter.write("constructor(\(constructorParamList)) {")
@@ -1945,7 +1875,7 @@ extension BridgeJSLink {
19451875
}
19461876

19471877
dtsExportEntryPrinter.indent {
1948-
let jsDocLines = DefaultValueGenerator().generateJSDoc(for: constructor.parameters)
1878+
let jsDocLines = DefaultValueUtils.formatJSDoc(for: constructor.parameters)
19491879
for line in jsDocLines {
19501880
dtsExportEntryPrinter.write(line)
19511881
}
@@ -3173,6 +3103,88 @@ extension BridgeJSLink {
31733103
}
31743104
}
31753105

3106+
/// Utility enum for generating default value representations in JavaScript/TypeScript
3107+
enum DefaultValueUtils {
3108+
enum OutputFormat {
3109+
case javascript
3110+
case typescript
3111+
}
3112+
3113+
/// Generates default value representation for JavaScript or TypeScript
3114+
static func format(_ defaultValue: DefaultValue, as format: OutputFormat) -> String {
3115+
switch defaultValue {
3116+
case .string(let value):
3117+
let escapedValue =
3118+
format == .javascript
3119+
? escapeForJavaScript(value)
3120+
: value // TypeScript doesn't need escape in doc comments
3121+
return "\"\(escapedValue)\""
3122+
case .int(let value):
3123+
return "\(value)"
3124+
case .float(let value):
3125+
return "\(value)"
3126+
case .double(let value):
3127+
return "\(value)"
3128+
case .bool(let value):
3129+
return value ? "true" : "false"
3130+
case .null:
3131+
return "null"
3132+
case .enumCase(let enumName, let caseName):
3133+
let simpleName = enumName.components(separatedBy: ".").last ?? enumName
3134+
let jsEnumName = format == .javascript ? "\(simpleName)\(ExportedEnum.valuesSuffix)" : simpleName
3135+
return "\(jsEnumName).\(caseName.capitalizedFirstLetter)"
3136+
case .object(let className):
3137+
return "new \(className)()"
3138+
case .objectWithArguments(let className, let args):
3139+
let argStrings = args.map { arg in
3140+
Self.format(arg, as: format)
3141+
}
3142+
return "new \(className)(\(argStrings.joined(separator: ", ")))"
3143+
case .structLiteral(_, let fields):
3144+
let fieldStrings = fields.map { field in
3145+
"\(field.name): \(Self.format(field.value, as: format))"
3146+
}
3147+
return "{ \(fieldStrings.joined(separator: ", ")) }"
3148+
}
3149+
}
3150+
3151+
private static func escapeForJavaScript(_ string: String) -> String {
3152+
return
3153+
string
3154+
.replacingOccurrences(of: "\\", with: "\\\\")
3155+
.replacingOccurrences(of: "\"", with: "\\\"")
3156+
}
3157+
3158+
/// Generates JSDoc comment lines for parameters with default values
3159+
static func formatJSDoc(for parameters: [Parameter]) -> [String] {
3160+
let paramsWithDefaults = parameters.filter { $0.hasDefault }
3161+
guard !paramsWithDefaults.isEmpty else {
3162+
return []
3163+
}
3164+
3165+
var jsDocLines: [String] = ["/**"]
3166+
for param in paramsWithDefaults {
3167+
if let defaultValue = param.defaultValue {
3168+
let defaultDoc = format(defaultValue, as: .typescript)
3169+
jsDocLines.append(" * @param \(param.name) - Optional parameter (default: \(defaultDoc))")
3170+
}
3171+
}
3172+
jsDocLines.append(" */")
3173+
return jsDocLines
3174+
}
3175+
3176+
/// Generates a JavaScript parameter list with default values
3177+
static func formatParameterList(_ parameters: [Parameter]) -> String {
3178+
return parameters.map { param in
3179+
if let defaultValue = param.defaultValue {
3180+
let defaultJs = format(defaultValue, as: .javascript)
3181+
return "\(param.name) = \(defaultJs)"
3182+
}
3183+
return param.name
3184+
}.joined(separator: ", ")
3185+
}
3186+
}
3187+
31763188
struct BridgeJSLinkError: Error {
31773189
let message: String
31783190
}

Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2135,8 +2135,9 @@ struct IntrinsicJSFragment: Sendable {
21352135

21362136
// Attach instance methods to the struct instance
21372137
for method in structDef.methods where !method.effects.isStatic {
2138+
let paramList = DefaultValueUtils.formatParameterList(method.parameters)
21382139
printer.write(
2139-
"\(instanceVar).\(method.name) = function(\(method.parameters.map { $0.name }.joined(separator: ", "))) {"
2140+
"\(instanceVar).\(method.name) = function(\(paramList)) {"
21402141
)
21412142
printer.indent {
21422143
let methodScope = JSGlueVariableScope()

Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,17 @@ public enum SwiftEnumRawType: String, CaseIterable, Codable, Sendable {
160160
}
161161
}
162162

163+
/// Represents a struct field with name and default value for default parameter values
164+
public struct DefaultValueField: Codable, Equatable, Sendable {
165+
public let name: String
166+
public let value: DefaultValue
167+
168+
public init(name: String, value: DefaultValue) {
169+
self.name = name
170+
self.value = value
171+
}
172+
}
173+
163174
public enum DefaultValue: Codable, Equatable, Sendable {
164175
case string(String)
165176
case int(Int)
@@ -170,6 +181,7 @@ public enum DefaultValue: Codable, Equatable, Sendable {
170181
case enumCase(String, String) // enumName, caseName
171182
case object(String) // className for parameterless constructor
172183
case objectWithArguments(String, [DefaultValue]) // className, constructor argument values
184+
case structLiteral(String, [DefaultValueField]) // structName, field name/value pairs
173185
}
174186

175187
public struct Parameter: Codable, Equatable, Sendable {

0 commit comments

Comments
 (0)