Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 53 additions & 26 deletions Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -316,50 +316,77 @@ public class ExportSwift {
return nil
}

let className = calledExpr.baseName.text
let expectedClassName: String?
let typeName = calledExpr.baseName.text

let isStructType: Bool
let expectedTypeName: String?
switch type {
case .swiftHeapObject(let name):
expectedClassName = name.split(separator: ".").last.map(String.init)
case .optional(.swiftHeapObject(let name)):
expectedClassName = name.split(separator: ".").last.map(String.init)
case .swiftStruct(let name), .optional(.swiftStruct(let name)):
isStructType = true
expectedTypeName = name.split(separator: ".").last.map(String.init)
case .swiftHeapObject(let name), .optional(.swiftHeapObject(let name)):
isStructType = false
expectedTypeName = name.split(separator: ".").last.map(String.init)
default:
diagnose(
node: funcCall,
message: "Constructor calls are only supported for class types",
hint: "Parameter type should be a Swift class"
message: "Constructor calls are only supported for class and struct types",
hint: "Parameter type should be a Swift class or struct"
)
return nil
}

guard let expectedClassName = expectedClassName, className == expectedClassName else {
guard let expectedTypeName = expectedTypeName, typeName == expectedTypeName else {
diagnose(
node: funcCall,
message: "Constructor class name '\(className)' doesn't match parameter type",
message: "Constructor type name '\(typeName)' doesn't match parameter type",
hint: "Ensure the constructor matches the parameter type"
)
return nil
}

if funcCall.arguments.isEmpty {
return .object(className)
}

var constructorArgs: [DefaultValue] = []
for argument in funcCall.arguments {
guard let argValue = extractLiteralValue(from: argument.expression) else {
diagnose(
node: argument.expression,
message: "Constructor argument must be a literal value",
hint: "Use simple literals like \"text\", 42, true, false in constructor arguments"
)
return nil
if isStructType {
// For structs, extract field name/value pairs
var fields: [DefaultValueField] = []
for argument in funcCall.arguments {
guard let fieldName = argument.label?.text else {
diagnose(
node: argument,
message: "Struct initializer arguments must have labels",
hint: "Use labeled arguments like MyStruct(x: 1, y: 2)"
)
return nil
}
guard let fieldValue = extractLiteralValue(from: argument.expression) else {
diagnose(
node: argument.expression,
message: "Struct field value must be a literal",
hint: "Use simple literals like \"text\", 42, true, false in struct fields"
)
return nil
}
fields.append(DefaultValueField(name: fieldName, value: fieldValue))
}
return .structLiteral(typeName, fields)
} else {
if funcCall.arguments.isEmpty {
return .object(typeName)
}

constructorArgs.append(argValue)
var constructorArgs: [DefaultValue] = []
for argument in funcCall.arguments {
guard let argValue = extractLiteralValue(from: argument.expression) else {
diagnose(
node: argument.expression,
message: "Constructor argument must be a literal value",
hint: "Use simple literals like \"text\", 42, true, false in constructor arguments"
)
return nil
}
constructorArgs.append(argValue)
}
return .objectWithArguments(typeName, constructorArgs)
}

return .objectWithArguments(className, constructorArgs)
}

/// Extracts a literal value from an expression with optional type checking
Expand Down
174 changes: 93 additions & 81 deletions Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1254,16 +1254,6 @@ struct BridgeJSLink {
]
}

func generateParameterList(parameters: [Parameter]) -> String {
parameters.map { param in
if let defaultValue = param.defaultValue {
let defaultJs = DefaultValueGenerator().generate(defaultValue, format: .javascript)
return "\(param.name) = \(defaultJs)"
}
return param.name
}.joined(separator: ", ")
}

func renderFunction(
name: String,
parameters: [Parameter],
Expand All @@ -1272,7 +1262,7 @@ struct BridgeJSLink {
) -> [String] {
let printer = CodeFragmentPrinter()

let parameterList = generateParameterList(parameters: parameters)
let parameterList = DefaultValueUtils.formatParameterList(parameters)

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

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

/// Helper struct for generating default value representations
private struct DefaultValueGenerator {
enum OutputFormat {
case javascript
case typescript
}

/// Generates default value representation for JavaScript or TypeScript
func generate(_ defaultValue: DefaultValue, format: OutputFormat) -> String {
switch defaultValue {
case .string(let value):
let escapedValue =
format == .javascript
? escapeForJavaScript(value)
: value // TypeScript doesn't need escape in doc comments
return "\"\(escapedValue)\""
case .int(let value):
return "\(value)"
case .float(let value):
return "\(value)"
case .double(let value):
return "\(value)"
case .bool(let value):
return value ? "true" : "false"
case .null:
return "null"
case .enumCase(let enumName, let caseName):
let simpleName = enumName.components(separatedBy: ".").last ?? enumName
let jsEnumName = format == .javascript ? "\(simpleName)\(ExportedEnum.valuesSuffix)" : simpleName
return "\(jsEnumName).\(caseName.capitalizedFirstLetter)"
case .object(let className):
return "new \(className)()"
case .objectWithArguments(let className, let args):
let argStrings = args.map { arg in
generate(arg, format: format)
}
return "new \(className)(\(argStrings.joined(separator: ", ")))"
}
}

private func escapeForJavaScript(_ string: String) -> String {
return
string
.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "\"", with: "\\\"")
}

/// Generates JSDoc comment lines for parameters with default values
func generateJSDoc(for parameters: [Parameter]) -> [String] {
let paramsWithDefaults = parameters.filter { $0.hasDefault }
guard !paramsWithDefaults.isEmpty else {
return []
}

var jsDocLines: [String] = ["/**"]
for param in paramsWithDefaults {
if let defaultValue = param.defaultValue {
let defaultDoc = generate(defaultValue, format: .typescript)
jsDocLines.append(" * @param \(param.name) - Optional parameter (default: \(defaultDoc))")
}
}
jsDocLines.append(" */")
return jsDocLines
}
}

func renderExportedStruct(
_ structDefinition: ExportedStruct
) throws -> (js: [String], dtsType: [String], dtsExportEntry: [String]) {
Expand All @@ -1437,6 +1361,8 @@ struct BridgeJSLink {
dtsTypePrinter.write("\(property.name): \(tsType);")
}
for method in structDefinition.methods where !method.effects.isStatic {
let jsDocLines = DefaultValueUtils.formatJSDoc(for: method.parameters)
dtsTypePrinter.write(lines: jsDocLines)
let signature = renderTSSignature(
parameters: method.parameters,
returnType: method.returnType,
Expand Down Expand Up @@ -1466,7 +1392,7 @@ struct BridgeJSLink {
)

let constructorPrinter = CodeFragmentPrinter()
let paramList = thunkBuilder.generateParameterList(parameters: constructor.parameters)
let paramList = DefaultValueUtils.formatParameterList(constructor.parameters)
constructorPrinter.write("init: function(\(paramList)) {")
constructorPrinter.indent {
constructorPrinter.write(contentsOf: thunkBuilder.body)
Expand Down Expand Up @@ -1499,6 +1425,8 @@ struct BridgeJSLink {
dtsExportEntryPrinter.write("\(structName): {")
dtsExportEntryPrinter.indent {
if let constructor = structDefinition.constructor {
let jsDocLines = DefaultValueUtils.formatJSDoc(for: constructor.parameters)
dtsExportEntryPrinter.write(lines: jsDocLines)
dtsExportEntryPrinter.write(
"init\(renderTSSignature(parameters: constructor.parameters, returnType: .swiftStruct(structDefinition.swiftCallName), effects: constructor.effects));"
)
Expand All @@ -1508,6 +1436,8 @@ struct BridgeJSLink {
dtsExportEntryPrinter.write("\(readonly)\(property.name): \(resolveTypeScriptType(property.type));")
}
for method in staticMethods {
let jsDocLines = DefaultValueUtils.formatJSDoc(for: method.parameters)
dtsExportEntryPrinter.write(lines: jsDocLines)
dtsExportEntryPrinter.write(
"\(method.name)\(renderTSSignature(parameters: method.parameters, returnType: method.returnType, effects: method.effects));"
)
Expand Down Expand Up @@ -1930,7 +1860,7 @@ extension BridgeJSLink {
try thunkBuilder.lowerParameter(param: param)
}

let constructorParamList = thunkBuilder.generateParameterList(parameters: constructor.parameters)
let constructorParamList = DefaultValueUtils.formatParameterList(constructor.parameters)

jsPrinter.indent {
jsPrinter.write("constructor(\(constructorParamList)) {")
Expand All @@ -1945,7 +1875,7 @@ extension BridgeJSLink {
}

dtsExportEntryPrinter.indent {
let jsDocLines = DefaultValueGenerator().generateJSDoc(for: constructor.parameters)
let jsDocLines = DefaultValueUtils.formatJSDoc(for: constructor.parameters)
for line in jsDocLines {
dtsExportEntryPrinter.write(line)
}
Expand Down Expand Up @@ -3173,6 +3103,88 @@ extension BridgeJSLink {
}
}

/// Utility enum for generating default value representations in JavaScript/TypeScript
enum DefaultValueUtils {
enum OutputFormat {
case javascript
case typescript
}

/// Generates default value representation for JavaScript or TypeScript
static func format(_ defaultValue: DefaultValue, as format: OutputFormat) -> String {
switch defaultValue {
case .string(let value):
let escapedValue =
format == .javascript
? escapeForJavaScript(value)
: value // TypeScript doesn't need escape in doc comments
return "\"\(escapedValue)\""
case .int(let value):
return "\(value)"
case .float(let value):
return "\(value)"
case .double(let value):
return "\(value)"
case .bool(let value):
return value ? "true" : "false"
case .null:
return "null"
case .enumCase(let enumName, let caseName):
let simpleName = enumName.components(separatedBy: ".").last ?? enumName
let jsEnumName = format == .javascript ? "\(simpleName)\(ExportedEnum.valuesSuffix)" : simpleName
return "\(jsEnumName).\(caseName.capitalizedFirstLetter)"
case .object(let className):
return "new \(className)()"
case .objectWithArguments(let className, let args):
let argStrings = args.map { arg in
Self.format(arg, as: format)
}
return "new \(className)(\(argStrings.joined(separator: ", ")))"
case .structLiteral(_, let fields):
let fieldStrings = fields.map { field in
"\(field.name): \(Self.format(field.value, as: format))"
}
return "{ \(fieldStrings.joined(separator: ", ")) }"
}
}

private static func escapeForJavaScript(_ string: String) -> String {
return
string
.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "\"", with: "\\\"")
}

/// Generates JSDoc comment lines for parameters with default values
static func formatJSDoc(for parameters: [Parameter]) -> [String] {
let paramsWithDefaults = parameters.filter { $0.hasDefault }
guard !paramsWithDefaults.isEmpty else {
return []
}

var jsDocLines: [String] = ["/**"]
for param in paramsWithDefaults {
if let defaultValue = param.defaultValue {
let defaultDoc = format(defaultValue, as: .typescript)
jsDocLines.append(" * @param \(param.name) - Optional parameter (default: \(defaultDoc))")
}
}
jsDocLines.append(" */")
return jsDocLines
}

/// Generates a JavaScript parameter list with default values
static func formatParameterList(_ parameters: [Parameter]) -> String {
return parameters.map { param in
if let defaultValue = param.defaultValue {
let defaultJs = format(defaultValue, as: .javascript)
return "\(param.name) = \(defaultJs)"
}
return param.name
}.joined(separator: ", ")
}
}

struct BridgeJSLinkError: Error {
let message: String
}
Expand Down
3 changes: 2 additions & 1 deletion Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2135,8 +2135,9 @@ struct IntrinsicJSFragment: Sendable {

// Attach instance methods to the struct instance
for method in structDef.methods where !method.effects.isStatic {
let paramList = DefaultValueUtils.formatParameterList(method.parameters)
printer.write(
"\(instanceVar).\(method.name) = function(\(method.parameters.map { $0.name }.joined(separator: ", "))) {"
"\(instanceVar).\(method.name) = function(\(paramList)) {"
)
printer.indent {
let methodScope = JSGlueVariableScope()
Expand Down
12 changes: 12 additions & 0 deletions Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,17 @@ public enum SwiftEnumRawType: String, CaseIterable, Codable, Sendable {
}
}

/// Represents a struct field with name and default value for default parameter values
public struct DefaultValueField: Codable, Equatable, Sendable {
public let name: String
public let value: DefaultValue

public init(name: String, value: DefaultValue) {
self.name = name
self.value = value
}
}

public enum DefaultValue: Codable, Equatable, Sendable {
case string(String)
case int(Int)
Expand All @@ -170,6 +181,7 @@ public enum DefaultValue: Codable, Equatable, Sendable {
case enumCase(String, String) // enumName, caseName
case object(String) // className for parameterless constructor
case objectWithArguments(String, [DefaultValue]) // className, constructor argument values
case structLiteral(String, [DefaultValueField]) // structName, field name/value pairs
}

public struct Parameter: Codable, Equatable, Sendable {
Expand Down
Loading