Skip to content

Commit c6353c4

Browse files
Merge pull request #453 from PassiveLogic/feat/default-values
[BridgeJS] Support default values
2 parents 2bd6e38 + e144554 commit c6353c4

File tree

16 files changed

+3130
-69
lines changed

16 files changed

+3130
-69
lines changed

Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Lines changed: 259 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,240 @@ 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 memberExpr = expr.as(MemberAccessExprSyntax.self),
247+
let enumValue = extractEnumCaseValue(from: memberExpr, type: type)
248+
{
249+
return enumValue
250+
}
251+
252+
if let funcCall = expr.as(FunctionCallExprSyntax.self) {
253+
return extractConstructorDefaultValue(from: funcCall, type: type)
254+
}
255+
256+
if let literalValue = extractLiteralValue(from: expr, type: type) {
257+
return literalValue
258+
}
259+
260+
diagnose(
261+
node: expr,
262+
message: "Unsupported default parameter value expression",
263+
hint: "Use simple literal values like \"text\", 42, true, false, nil, or enum cases like .caseName"
264+
)
265+
return nil
266+
}
267+
268+
/// Extracts default value from a constructor call expression
269+
private func extractConstructorDefaultValue(
270+
from funcCall: FunctionCallExprSyntax,
271+
type: BridgeType
272+
) -> DefaultValue? {
273+
guard let calledExpr = funcCall.calledExpression.as(DeclReferenceExprSyntax.self) else {
274+
diagnose(
275+
node: funcCall,
276+
message: "Complex constructor expressions are not supported",
277+
hint: "Use a simple constructor call like ClassName() or ClassName(arg: value)"
278+
)
279+
return nil
280+
}
281+
282+
let className = calledExpr.baseName.text
283+
let expectedClassName: String?
284+
switch type {
285+
case .swiftHeapObject(let name):
286+
expectedClassName = name.split(separator: ".").last.map(String.init)
287+
case .optional(.swiftHeapObject(let name)):
288+
expectedClassName = name.split(separator: ".").last.map(String.init)
289+
default:
290+
diagnose(
291+
node: funcCall,
292+
message: "Constructor calls are only supported for class types",
293+
hint: "Parameter type should be a Swift class"
294+
)
295+
return nil
296+
}
297+
298+
guard let expectedClassName = expectedClassName, className == expectedClassName else {
299+
diagnose(
300+
node: funcCall,
301+
message: "Constructor class name '\(className)' doesn't match parameter type",
302+
hint: "Ensure the constructor matches the parameter type"
303+
)
304+
return nil
305+
}
306+
307+
if funcCall.arguments.isEmpty {
308+
return .object(className)
309+
}
310+
311+
var constructorArgs: [DefaultValue] = []
312+
for argument in funcCall.arguments {
313+
guard let argValue = extractLiteralValue(from: argument.expression) else {
314+
diagnose(
315+
node: argument.expression,
316+
message: "Constructor argument must be a literal value",
317+
hint: "Use simple literals like \"text\", 42, true, false in constructor arguments"
318+
)
319+
return nil
320+
}
321+
322+
constructorArgs.append(argValue)
323+
}
324+
325+
return .objectWithArguments(className, constructorArgs)
326+
}
327+
328+
/// Extracts a literal value from an expression with optional type checking
329+
private func extractLiteralValue(from expr: ExprSyntax, type: BridgeType? = nil) -> DefaultValue? {
330+
if expr.is(NilLiteralExprSyntax.self) {
331+
return .null
332+
}
333+
334+
if let stringLiteral = expr.as(StringLiteralExprSyntax.self),
335+
let segment = stringLiteral.segments.first?.as(StringSegmentSyntax.self)
336+
{
337+
let value = DefaultValue.string(segment.content.text)
338+
if let type = type, !type.isCompatibleWith(.string) {
339+
return nil
340+
}
341+
return value
342+
}
343+
344+
if let boolLiteral = expr.as(BooleanLiteralExprSyntax.self) {
345+
let value = DefaultValue.bool(boolLiteral.literal.text == "true")
346+
if let type = type, !type.isCompatibleWith(.bool) {
347+
return nil
348+
}
349+
return value
350+
}
351+
352+
var numericExpr = expr
353+
var isNegative = false
354+
if let prefixExpr = expr.as(PrefixOperatorExprSyntax.self),
355+
prefixExpr.operator.text == "-"
356+
{
357+
numericExpr = prefixExpr.expression
358+
isNegative = true
359+
}
360+
361+
if let intLiteral = numericExpr.as(IntegerLiteralExprSyntax.self),
362+
let intValue = Int(intLiteral.literal.text)
363+
{
364+
let value = DefaultValue.int(isNegative ? -intValue : intValue)
365+
if let type = type, !type.isCompatibleWith(.int) {
366+
return nil
367+
}
368+
return value
369+
}
370+
371+
if let floatLiteral = numericExpr.as(FloatLiteralExprSyntax.self) {
372+
if let floatValue = Float(floatLiteral.literal.text) {
373+
let value = DefaultValue.float(isNegative ? -floatValue : floatValue)
374+
if type == nil || type?.isCompatibleWith(.float) == true {
375+
return value
376+
}
377+
}
378+
if let doubleValue = Double(floatLiteral.literal.text) {
379+
let value = DefaultValue.double(isNegative ? -doubleValue : doubleValue)
380+
if type == nil || type?.isCompatibleWith(.double) == true {
381+
return value
382+
}
383+
}
384+
}
385+
386+
return nil
387+
}
388+
155389
override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
156390
guard node.attributes.hasJSAttribute() else {
157391
return .skipChildren
@@ -252,7 +486,10 @@ public class ExportSwift {
252486

253487
let name = param.secondName?.text ?? param.firstName.text
254488
let label = param.firstName.text
255-
parameters.append(Parameter(label: label, name: name, type: type))
489+
490+
let defaultValue = extractDefaultValue(from: param.defaultValue, type: type)
491+
492+
parameters.append(Parameter(label: label, name: name, type: type, defaultValue: defaultValue))
256493
}
257494
let returnType: BridgeType
258495
if let returnClause = node.signature.returnClause {
@@ -409,7 +646,10 @@ public class ExportSwift {
409646
}
410647
let name = param.secondName?.text ?? param.firstName.text
411648
let label = param.firstName.text
412-
parameters.append(Parameter(label: label, name: name, type: type))
649+
650+
let defaultValue = extractDefaultValue(from: param.defaultValue, type: type)
651+
652+
parameters.append(Parameter(label: label, name: name, type: type, defaultValue: defaultValue))
413653
}
414654

415655
guard let effects = collectEffects(signature: node.signature) else {
@@ -630,7 +870,7 @@ public class ExportSwift {
630870
swiftCallName: swiftCallName,
631871
explicitAccessControl: explicitAccessControl,
632872
cases: [], // Will be populated in visit(EnumCaseDeclSyntax)
633-
rawType: rawType,
873+
rawType: SwiftEnumRawType(rawType),
634874
namespace: effectiveNamespace,
635875
emitStyle: emitStyle,
636876
staticMethods: [],
@@ -668,9 +908,7 @@ public class ExportSwift {
668908

669909
if case .tsEnum = emitStyle {
670910
// Check for Bool raw type limitation
671-
if let raw = exportedEnum.rawType,
672-
let rawEnum = SwiftEnumRawType.from(raw), rawEnum == .bool
673-
{
911+
if exportedEnum.rawType == .bool {
674912
diagnose(
675913
node: jsAttribute,
676914
message: "TypeScript enum style is not supported for Bool raw-value enums",
@@ -925,7 +1163,7 @@ public class ExportSwift {
9251163
return Constants.supportedRawTypes.contains(typeName)
9261164
}?.type.trimmedDescription
9271165

928-
if let rawTypeString, let rawType = SwiftEnumRawType.from(rawTypeString) {
1166+
if let rawType = SwiftEnumRawType(rawTypeString) {
9291167
return .rawValueEnum(swiftCallName, rawType)
9301168
} else {
9311169
let hasAnyCases = enumDecl.memberBlock.members.contains { member in
@@ -1903,3 +2141,17 @@ extension WithModifiersSyntax {
19032141
}
19042142
}
19052143
}
2144+
2145+
fileprivate extension BridgeType {
2146+
/// Returns true if a value of `expectedType` can be assigned to this type.
2147+
func isCompatibleWith(_ expectedType: BridgeType) -> Bool {
2148+
switch (self, expectedType) {
2149+
case let (lhs, rhs) where lhs == rhs:
2150+
return true
2151+
case (.optional(let wrapped), expectedType):
2152+
return wrapped == expectedType
2153+
default:
2154+
return false
2155+
}
2156+
}
2157+
}

0 commit comments

Comments
 (0)