Skip to content

Commit 451c248

Browse files
committed
BridgeJS: WIP default values
1 parent 2bd6e38 commit 451c248

File tree

15 files changed

+2295
-10
lines changed

15 files changed

+2295
-10
lines changed

Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Lines changed: 270 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,255 @@ public class ExportSwift {
152152
)
153153
}
154154

155+
/// Detects whether given expression is supported as default parameter value
156+
private func isSupportedDefaultValueExpression(_ initClause: InitializerClauseSyntax) -> Bool {
157+
let expression = initClause.value
158+
159+
// Function calls are checked later in extractDefaultValue (as constructors are allowed)
160+
if expression.is(ArrayExprSyntax.self) { return false }
161+
if expression.is(DictionaryExprSyntax.self) { return false }
162+
if expression.is(BinaryOperatorExprSyntax.self) { return false }
163+
if expression.is(ClosureExprSyntax.self) { return false }
164+
165+
// Method call chains (e.g., obj.foo())
166+
if let memberExpression = expression.as(MemberAccessExprSyntax.self),
167+
memberExpression.base?.is(FunctionCallExprSyntax.self) == true
168+
{
169+
return false
170+
}
171+
172+
return true
173+
}
174+
175+
/// Extract enum case value from member access expression
176+
private func extractEnumCaseValue(
177+
from memberExpr: MemberAccessExprSyntax,
178+
type: BridgeType
179+
) -> DefaultValue? {
180+
let caseName = memberExpr.declName.baseName.text
181+
182+
let enumName: String?
183+
switch type {
184+
case .caseEnum(let name), .rawValueEnum(let name, _), .associatedValueEnum(let name):
185+
enumName = name
186+
case .optional(let wrappedType):
187+
switch wrappedType {
188+
case .caseEnum(let name), .rawValueEnum(let name, _), .associatedValueEnum(let name):
189+
enumName = name
190+
default:
191+
return nil
192+
}
193+
default:
194+
return nil
195+
}
196+
197+
guard let enumName = enumName else { return nil }
198+
199+
if memberExpr.base == nil {
200+
return .enumCase(enumName, caseName)
201+
}
202+
203+
if let baseExpr = memberExpr.base?.as(DeclReferenceExprSyntax.self) {
204+
let baseName = baseExpr.baseName.text
205+
let lastComponent = enumName.split(separator: ".").last.map(String.init) ?? enumName
206+
if baseName == enumName || baseName == lastComponent {
207+
return .enumCase(enumName, caseName)
208+
}
209+
}
210+
211+
return nil
212+
}
213+
214+
/// Extracts default value from parameter's default value clause
215+
private func extractDefaultValue(
216+
from defaultClause: InitializerClauseSyntax?,
217+
type: BridgeType
218+
) -> DefaultValue? {
219+
guard let defaultClause = defaultClause else {
220+
return nil
221+
}
222+
223+
if !isSupportedDefaultValueExpression(defaultClause) {
224+
diagnose(
225+
node: defaultClause,
226+
message: "Complex default parameter expressions are not supported",
227+
hint: "Use simple literal values (e.g., \"text\", 42, true, nil) or simple constants"
228+
)
229+
return nil
230+
}
231+
232+
let expr = defaultClause.value
233+
234+
if expr.is(NilLiteralExprSyntax.self) {
235+
guard case .optional(_) = type else {
236+
diagnose(
237+
node: expr,
238+
message: "nil is only valid for optional parameters",
239+
hint: "Make the parameter optional by adding ? to the type"
240+
)
241+
return nil
242+
}
243+
return .null
244+
}
245+
246+
if let stringLiteral = expr.as(StringLiteralExprSyntax.self),
247+
let segment = stringLiteral.segments.first?.as(StringSegmentSyntax.self),
248+
type.matches(against: .string)
249+
{
250+
return .string(segment.content.text)
251+
}
252+
253+
if let boolLiteral = expr.as(BooleanLiteralExprSyntax.self),
254+
type.matches(against: .bool)
255+
{
256+
return .bool(boolLiteral.literal.text == "true")
257+
}
258+
259+
if let intLiteral = expr.as(IntegerLiteralExprSyntax.self),
260+
let intValue = Int(intLiteral.literal.text),
261+
type.matches(against: .int)
262+
{
263+
return .int(intValue)
264+
}
265+
266+
if let floatLiteral = expr.as(FloatLiteralExprSyntax.self) {
267+
if type.matches(against: .float),
268+
let floatValue = Float(floatLiteral.literal.text)
269+
{
270+
return .float(floatValue)
271+
}
272+
if type.matches(against: .double),
273+
let doubleValue = Double(floatLiteral.literal.text)
274+
{
275+
return .double(doubleValue)
276+
}
277+
}
278+
279+
if let memberExpr = expr.as(MemberAccessExprSyntax.self),
280+
let enumValue = extractEnumCaseValue(from: memberExpr, type: type)
281+
{
282+
return enumValue
283+
}
284+
285+
// Constructor calls (e.g., Greeter(name: "John"))
286+
if let funcCall = expr.as(FunctionCallExprSyntax.self) {
287+
return extractConstructorDefaultValue(from: funcCall, type: type)
288+
}
289+
290+
diagnose(
291+
node: expr,
292+
message: "Unsupported default parameter value expression",
293+
hint: "Use simple literal values like \"text\", 42, true, false, nil, or enum cases like .caseName"
294+
)
295+
return nil
296+
}
297+
298+
/// Extracts default value from a constructor call expression
299+
private func extractConstructorDefaultValue(
300+
from funcCall: FunctionCallExprSyntax,
301+
type: BridgeType
302+
) -> DefaultValue? {
303+
// Extract class name
304+
guard let calledExpr = funcCall.calledExpression.as(DeclReferenceExprSyntax.self) else {
305+
diagnose(
306+
node: funcCall,
307+
message: "Complex constructor expressions are not supported",
308+
hint: "Use a simple constructor call like ClassName() or ClassName(arg: value)"
309+
)
310+
return nil
311+
}
312+
313+
let className = calledExpr.baseName.text
314+
315+
// Verify type matches
316+
let expectedClassName: String?
317+
switch type {
318+
case .swiftHeapObject(let name):
319+
expectedClassName = name.split(separator: ".").last.map(String.init)
320+
case .optional(.swiftHeapObject(let name)):
321+
expectedClassName = name.split(separator: ".").last.map(String.init)
322+
default:
323+
diagnose(
324+
node: funcCall,
325+
message: "Constructor calls are only supported for class types",
326+
hint: "Parameter type should be a Swift class"
327+
)
328+
return nil
329+
}
330+
331+
guard let expectedClassName = expectedClassName, className == expectedClassName else {
332+
diagnose(
333+
node: funcCall,
334+
message: "Constructor class name '\(className)' doesn't match parameter type",
335+
hint: "Ensure the constructor matches the parameter type"
336+
)
337+
return nil
338+
}
339+
340+
// Handle parameterless constructor
341+
if funcCall.arguments.isEmpty {
342+
return .object(className)
343+
}
344+
345+
// Extract arguments for constructor with parameters
346+
var constructorArgs: [DefaultValue] = []
347+
for argument in funcCall.arguments {
348+
// Recursively extract the argument's default value
349+
// For now, only support literals in constructor arguments
350+
guard let argValue = extractConstructorArgumentValue(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
357+
}
358+
359+
constructorArgs.append(argValue)
360+
}
361+
362+
return .objectWithArguments(className, constructorArgs)
363+
}
364+
365+
/// Extracts a literal value from an expression for use in constructor arguments
366+
private func extractConstructorArgumentValue(from expr: ExprSyntax) -> DefaultValue? {
367+
// String literals
368+
if let stringLiteral = expr.as(StringLiteralExprSyntax.self),
369+
let segment = stringLiteral.segments.first?.as(StringSegmentSyntax.self)
370+
{
371+
return .string(segment.content.text)
372+
}
373+
374+
// Boolean literals
375+
if let boolLiteral = expr.as(BooleanLiteralExprSyntax.self) {
376+
return .bool(boolLiteral.literal.text == "true")
377+
}
378+
379+
// Integer literals
380+
if let intLiteral = expr.as(IntegerLiteralExprSyntax.self),
381+
let intValue = Int(intLiteral.literal.text)
382+
{
383+
return .int(intValue)
384+
}
385+
386+
// Float literals
387+
if let floatLiteral = expr.as(FloatLiteralExprSyntax.self) {
388+
if let floatValue = Float(floatLiteral.literal.text) {
389+
return .float(floatValue)
390+
}
391+
if let doubleValue = Double(floatLiteral.literal.text) {
392+
return .double(doubleValue)
393+
}
394+
}
395+
396+
// nil literal
397+
if expr.is(NilLiteralExprSyntax.self) {
398+
return .null
399+
}
400+
401+
return nil
402+
}
403+
155404
override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
156405
guard node.attributes.hasJSAttribute() else {
157406
return .skipChildren
@@ -252,7 +501,10 @@ public class ExportSwift {
252501

253502
let name = param.secondName?.text ?? param.firstName.text
254503
let label = param.firstName.text
255-
parameters.append(Parameter(label: label, name: name, type: type))
504+
505+
let defaultValue = extractDefaultValue(from: param.defaultValue, type: type)
506+
507+
parameters.append(Parameter(label: label, name: name, type: type, defaultValue: defaultValue))
256508
}
257509
let returnType: BridgeType
258510
if let returnClause = node.signature.returnClause {
@@ -409,7 +661,10 @@ public class ExportSwift {
409661
}
410662
let name = param.secondName?.text ?? param.firstName.text
411663
let label = param.firstName.text
412-
parameters.append(Parameter(label: label, name: name, type: type))
664+
665+
let defaultValue = extractDefaultValue(from: param.defaultValue, type: type)
666+
667+
parameters.append(Parameter(label: label, name: name, type: type, defaultValue: defaultValue))
413668
}
414669

415670
guard let effects = collectEffects(signature: node.signature) else {
@@ -1903,3 +2158,16 @@ extension WithModifiersSyntax {
19032158
}
19042159
}
19052160
}
2161+
2162+
fileprivate extension BridgeType {
2163+
func matches(against expected: BridgeType) -> Bool {
2164+
switch (self, expected) {
2165+
case let (lhs, rhs) where lhs == rhs:
2166+
return true
2167+
case (.optional(let wrapped), expected):
2168+
return wrapped == expected
2169+
default:
2170+
return false
2171+
}
2172+
}
2173+
}

0 commit comments

Comments
 (0)