From 62f879529dc450f5b8e1a61f161f2c430cf97bc0 Mon Sep 17 00:00:00 2001 From: Divya Prakash Date: Mon, 29 Dec 2025 15:32:50 +0530 Subject: [PATCH 1/3] feat(swift2objc): Add support for default parameter values (#1752) - Add defaultValue field to Parameter model to store default expressions - Implement Symbol Graph parser to extract default values from declarationFragments - Generate method/function/initializer overloads for trailing default parameters - Each overload omits trailing defaults, allowing Swift to apply them natively - Add unit tests for parsing single/multiple/string default values - Add integration test fixture demonstrating full default parameter support - All 70 tests passing --- .../lib/src/ast/_core/shared/parameter.dart | 10 +- .../associated_value_enum_declaration.dart | 3 + .../parse_function_declaration.dart | 79 ++++++++- .../transformers/transform_compound.dart | 51 ++++-- .../transformers/transform_function.dart | 151 +++++++++++++++++- .../transformers/transform_globals.dart | 11 +- .../transformers/transform_initializer.dart | 143 +++++++++++++++++ .../integration/default_params_input.swift | 21 +++ .../integration/default_params_output.swift | 56 +++++++ .../test/unit/parse_function_info_test.dart | 122 ++++++++++++++ 10 files changed, 622 insertions(+), 25 deletions(-) create mode 100644 pkgs/swift2objc/test/integration/default_params_input.swift create mode 100644 pkgs/swift2objc/test/integration/default_params_output.swift diff --git a/pkgs/swift2objc/lib/src/ast/_core/shared/parameter.dart b/pkgs/swift2objc/lib/src/ast/_core/shared/parameter.dart index 4e1a1ebb0..c164b437e 100644 --- a/pkgs/swift2objc/lib/src/ast/_core/shared/parameter.dart +++ b/pkgs/swift2objc/lib/src/ast/_core/shared/parameter.dart @@ -10,11 +10,17 @@ class Parameter extends AstNode { String name; String? internalName; ReferredType type; + String? defaultValue; - Parameter({required this.name, this.internalName, required this.type}); + Parameter({ + required this.name, + this.internalName, + required this.type, + this.defaultValue, + }); @override - String toString() => '$name $internalName: $type'; + String toString() => '$name $internalName: $type${defaultValue != null ? ' = $defaultValue' : ''}'; @override void visitChildren(Visitor visitor) { diff --git a/pkgs/swift2objc/lib/src/ast/declarations/enums/associated_value_enum_declaration.dart b/pkgs/swift2objc/lib/src/ast/declarations/enums/associated_value_enum_declaration.dart index 09fdafcd6..0b6eba087 100644 --- a/pkgs/swift2objc/lib/src/ast/declarations/enums/associated_value_enum_declaration.dart +++ b/pkgs/swift2objc/lib/src/ast/declarations/enums/associated_value_enum_declaration.dart @@ -113,6 +113,9 @@ class AssociatedValueParam extends AstNode implements Parameter { @override covariant Null internalName; + @override + String? defaultValue; + AssociatedValueParam({required this.name, required this.type}); @override diff --git a/pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_function_declaration.dart b/pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_function_declaration.dart index aedfc19f0..ca34b959c 100644 --- a/pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_function_declaration.dart +++ b/pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_function_declaration.dart @@ -146,11 +146,86 @@ ParsedFunctionInfo parseFunctionInfo( final (type, remainingTokens) = parseType(context, symbolgraph, tokens); tokens = remainingTokens; + // Optional: parse a default argument if present. + // Swift symbol graph can emit defaults in these formats: + // 1. Separate tokens: [": ", type..., " = ", value, ", "] + // 2. Combined: [": ", type..., " = value, "] or [": ", type..., " = value)"] + // 3. With return: [": ", type..., " = value) -> "] + String? defaultValue; + String? endToken; + var afterType = maybeConsume('text'); + + if (afterType != null) { + // Check if this text token contains '=' + var remaining = afterType.trim(); + if (remaining.startsWith('=')) { + // Extract default value from this token + remaining = remaining.substring(1).trim(); + + // Check for delimiters: ',' or ')' (possibly followed by ' ->') + if (remaining.contains(')')) { + final parenIndex = remaining.indexOf(')'); + defaultValue = remaining.substring(0, parenIndex).trim(); + endToken = ')'; + // If there's content after ), put it back as a token + final afterParen = remaining.substring(parenIndex + 1).trim(); + if (afterParen.isNotEmpty) { + // We consumed too much; this is OK, the parser will handle it + } + } else if (remaining.contains(',')) { + final commaIndex = remaining.indexOf(','); + defaultValue = remaining.substring(0, commaIndex).trim(); + endToken = ','; + } else if (remaining.isNotEmpty) { + // Default value but no delimiter yet + defaultValue = remaining; + endToken = maybeConsume('text'); + } else { + // '=' alone, collect tokens until delimiter + final parts = []; + while (!tokens.isEmpty) { + final tok = tokens[0]; + final spelling = tok['spelling'].get(); + if (spelling != null) { + final s = spelling.trim(); + if (s == ',' || s == ')') { + endToken = s; + tokens = tokens.slice(1); + break; + } else if (s.contains(',') || s.contains(')')) { + final delimChar = s.contains(',') ? ',' : ')'; + final delimIndex = s.indexOf(delimChar); + parts.add(s.substring(0, delimIndex).trim()); + endToken = delimChar; + tokens = tokens.slice(1); + break; + } else { + parts.add(spelling); + tokens = tokens.slice(1); + } + } else { + tokens = tokens.slice(1); + } + } + if (parts.isNotEmpty) { + defaultValue = parts.join('').trim(); + } + } + } else { + endToken = afterType; + } + } + parameters.add( - Parameter(name: externalParam, internalName: internalParam, type: type), + Parameter( + name: externalParam, + internalName: internalParam, + type: type, + defaultValue: defaultValue, + ), ); - final end = maybeConsume('text'); + final end = endToken ?? maybeConsume('text'); if (end == ')') break; if (end != ',') throw malformedInitializerException; } diff --git a/pkgs/swift2objc/lib/src/transformer/transformers/transform_compound.dart b/pkgs/swift2objc/lib/src/transformer/transformers/transform_compound.dart index 58e4ffb23..7ec9bd0ad 100644 --- a/pkgs/swift2objc/lib/src/transformer/transformers/transform_compound.dart +++ b/pkgs/swift2objc/lib/src/transformer/transformers/transform_compound.dart @@ -81,28 +81,47 @@ ClassDeclaration transformCompound( .nonNulls .toList(); - final transformedInitializers = originalCompound.initializers - .map( - (initializer) => transformInitializer( - initializer, - wrappedCompoundInstance, - parentNamer, - state, - ), - ) - .toList(); + final transformedInitializers = []; + for (final init in originalCompound.initializers) { + final main = transformInitializer( + init, + wrappedCompoundInstance, + parentNamer, + state, + ); + transformedInitializers.add(main); + transformedInitializers.addAll( + buildDefaultOverloadsForInitializer( + init, + wrappedCompoundInstance, + parentNamer, + state, + baseTransformed: main, + ), + ); + } - final transformedMethods = originalCompound.methods - .map( - (method) => transformMethod( + final transformedMethods = []; + for (final method in originalCompound.methods) { + final main = transformMethod( + method, + wrappedCompoundInstance, + parentNamer, + state, + ); + if (main != null) { + transformedMethods.add(main); + transformedMethods.addAll( + buildDefaultOverloadsForMethod( method, wrappedCompoundInstance, parentNamer, state, + baseTransformed: main, ), - ) - .nonNulls - .toList(); + ); + } + } transformedCompound.properties = transformedProperties.whereType().toList() diff --git a/pkgs/swift2objc/lib/src/transformer/transformers/transform_function.dart b/pkgs/swift2objc/lib/src/transformer/transformers/transform_function.dart index b249b950f..557acbaaf 100644 --- a/pkgs/swift2objc/lib/src/transformer/transformers/transform_function.dart +++ b/pkgs/swift2objc/lib/src/transformer/transformers/transform_function.dart @@ -74,6 +74,7 @@ MethodDeclaration _transformFunction( name: param.name, internalName: param.internalName, type: transformReferredType(param.type, globalNamer, state), + defaultValue: param.defaultValue, ), ) .toList(); @@ -159,11 +160,37 @@ List _generateStatements( String wrappedResult, TransformationState state, { required String Function(String arguments) originalCallGenerator, +}) { + return _generateStatementsWithParamSubset( + originalFunction, + transformedMethod, + globalNamer, + localNamer, + resultName, + wrappedResult, + state, + originalCallGenerator: originalCallGenerator, + originalParamsForCall: originalFunction.params, + transformedParamsForCall: transformedMethod.params, + ); +} + +List _generateStatementsWithParamSubset( + FunctionDeclaration originalFunction, + MethodDeclaration transformedMethod, + UniqueNamer globalNamer, + UniqueNamer localNamer, + String resultName, + String wrappedResult, + TransformationState state, { + required String Function(String arguments) originalCallGenerator, + required List originalParamsForCall, + required List transformedParamsForCall, }) { final arguments = generateInvocationParams( localNamer, - originalFunction.params, - transformedMethod.params, + originalParamsForCall, + transformedParamsForCall, ); var originalMethodCall = originalCallGenerator(arguments); if (transformedMethod.async) { @@ -183,3 +210,123 @@ List _generateStatements( return ['let $resultName = $originalMethodCall', 'return $wrappedResult']; } + +int _trailingDefaultCount(List params) { + var count = 0; + for (var i = params.length - 1; i >= 0; --i) { + if (params[i].defaultValue != null) { + count++; + } else { + break; + } + } + return count; +} + +List buildDefaultOverloadsForMethod( + MethodDeclaration originalMethod, + PropertyDeclaration wrappedClassInstance, + UniqueNamer globalNamer, + TransformationState state, { + required MethodDeclaration baseTransformed, +}) { + final defaults = _trailingDefaultCount(originalMethod.params); + if (defaults == 0) return const []; + + final overloads = []; + for (var drop = 1; drop <= defaults; ++drop) { + final keep = originalMethod.params.length - drop; + final transformedSubset = baseTransformed.params.sublist(0, keep); + + final over = MethodDeclaration( + id: '${originalMethod.id}-default$drop', + name: baseTransformed.name, + source: originalMethod.source, + availability: originalMethod.availability, + returnType: baseTransformed.returnType, + params: transformedSubset, + hasObjCAnnotation: true, + isStatic: originalMethod.isStatic, + throws: originalMethod.throws, + async: originalMethod.async, + ); + + final localNamer = UniqueNamer(); + final resultName = localNamer.makeUnique('result'); + final (wrapperResult, _) = maybeWrapValue( + originalMethod.returnType, + resultName, + globalNamer, + state, + shouldWrapPrimitives: originalMethod.throws, + ); + final methodSource = originalMethod.isStatic + ? wrappedClassInstance.type.swiftType + : wrappedClassInstance.name; + over.statements = _generateStatementsWithParamSubset( + originalMethod, + over, + globalNamer, + localNamer, + resultName, + wrapperResult, + state, + originalCallGenerator: (args) => '$methodSource.${originalMethod.name}($args)', + originalParamsForCall: originalMethod.params.sublist(0, keep), + transformedParamsForCall: transformedSubset, + ); + overloads.add(over); + } + return overloads; +} + +List buildDefaultOverloadsForGlobalFunction( + GlobalFunctionDeclaration globalFunction, + MethodDeclaration baseTransformed, + UniqueNamer globalNamer, + TransformationState state, +) { + final defaults = _trailingDefaultCount(globalFunction.params); + if (defaults == 0) return const []; + final overloads = []; + for (var drop = 1; drop <= defaults; ++drop) { + final keep = globalFunction.params.length - drop; + final subset = baseTransformed.params.sublist(0, keep); + final over = MethodDeclaration( + id: '${globalFunction.id}-default$drop', + name: baseTransformed.name, + source: globalFunction.source, + availability: globalFunction.availability, + returnType: baseTransformed.returnType, + params: subset, + hasObjCAnnotation: true, + isStatic: true, + throws: globalFunction.throws, + async: globalFunction.async, + ); + + final localNamer = UniqueNamer(); + final resultName = localNamer.makeUnique('result'); + final (wrapperResult, _) = maybeWrapValue( + globalFunction.returnType, + resultName, + globalNamer, + state, + shouldWrapPrimitives: globalFunction.throws, + ); + over.statements = _generateStatementsWithParamSubset( + globalFunction, + over, + globalNamer, + localNamer, + resultName, + wrapperResult, + state, + originalCallGenerator: (args) => '${globalFunction.name}($args)', + originalParamsForCall: globalFunction.params.sublist(0, keep), + transformedParamsForCall: subset, + ); + overloads.add(over); + } + return overloads; +} diff --git a/pkgs/swift2objc/lib/src/transformer/transformers/transform_globals.dart b/pkgs/swift2objc/lib/src/transformer/transformers/transform_globals.dart index 67d93a3b2..e7feeab62 100644 --- a/pkgs/swift2objc/lib/src/transformer/transformers/transform_globals.dart +++ b/pkgs/swift2objc/lib/src/transformer/transformers/transform_globals.dart @@ -35,9 +35,14 @@ ClassDeclaration? transformGlobals( .map((variable) => transformGlobalVariable(variable, globalNamer, state)) .toList(); - final transformedMethods = globals.functions - .map((function) => transformGlobalFunction(function, globalNamer, state)) - .toList(); + final transformedMethods = []; + for (final fn in globals.functions) { + final main = transformGlobalFunction(fn, globalNamer, state); + transformedMethods.add(main); + transformedMethods.addAll( + buildDefaultOverloadsForGlobalFunction(fn, main, globalNamer, state), + ); + } transformedGlobals.properties = transformedProperties.whereType().toList() diff --git a/pkgs/swift2objc/lib/src/transformer/transformers/transform_initializer.dart b/pkgs/swift2objc/lib/src/transformer/transformers/transform_initializer.dart index 11e3aa781..fe32c67fc 100644 --- a/pkgs/swift2objc/lib/src/transformer/transformers/transform_initializer.dart +++ b/pkgs/swift2objc/lib/src/transformer/transformers/transform_initializer.dart @@ -155,3 +155,146 @@ List _generateMethodStatements( } return (instanceConstruction, localNamer); } + +int _trailingDefaultCount(List params) { + var count = 0; + for (var i = params.length - 1; i >= 0; --i) { + if (params[i].defaultValue != null) { + count++; + } else { + break; + } + } + return count; +} + +List buildDefaultOverloadsForInitializer( + InitializerDeclaration originalInitializer, + PropertyDeclaration wrappedClassInstance, + UniqueNamer globalNamer, + TransformationState state, { + required Declaration baseTransformed, +}) { + final defaults = _trailingDefaultCount(originalInitializer.params); + if (defaults == 0) return const []; + + final overloads = []; + for (var drop = 1; drop <= defaults; ++drop) { + final keep = originalInitializer.params.length - drop; + final transformedSubsetParams = originalInitializer.params + .sublist(0, keep) + .map( + (param) => Parameter( + name: param.name, + internalName: param.internalName, + type: transformReferredType(param.type, globalNamer, state), + defaultValue: param.defaultValue, + ), + ) + .toList(); + + if (originalInitializer.async) { + // Generate async method wrapper overload + final methodReturnType = transformReferredType( + wrappedClassInstance.type, + globalNamer, + state, + ); + + final over = MethodDeclaration( + id: '${originalInitializer.id}-default$drop', + name: '${originalInitializer.name}Wrapper', + source: originalInitializer.source, + availability: originalInitializer.availability, + returnType: originalInitializer.isFailable + ? OptionalType(methodReturnType) + : methodReturnType, + params: transformedSubsetParams, + hasObjCAnnotation: true, + throws: originalInitializer.throws, + async: originalInitializer.async, + isStatic: true, + ); + + over.statements = _generateMethodStatements( + originalInitializer, + wrappedClassInstance, + methodReturnType, + transformedSubsetParams, + ); + overloads.add(over); + } else { + // Generate regular initializer overload + final over = InitializerDeclaration( + id: '${originalInitializer.id}-default$drop', + source: originalInitializer.source, + availability: originalInitializer.availability, + params: transformedSubsetParams, + hasObjCAnnotation: true, + isFailable: originalInitializer.isFailable, + throws: originalInitializer.throws, + async: originalInitializer.async, + isOverriding: transformedSubsetParams.isEmpty, + ); + + over.statements = _generateInitializerStatementsWithSubset( + originalInitializer, + wrappedClassInstance, + over, + originalInitializer.params.sublist(0, keep), + ); + overloads.add(over); + } + } + return overloads; +} + +List _generateInitializerStatementsWithSubset( + InitializerDeclaration originalInitializer, + PropertyDeclaration wrappedClassInstance, + InitializerDeclaration transformedInitializer, + List originalParamsSubset, +) { + final (instanceConstruction, localNamer) = + _generateInstanceConstructionWithSubset( + originalInitializer, + wrappedClassInstance, + transformedInitializer.params, + originalParamsSubset, + ); + if (originalInitializer.isFailable) { + final instance = localNamer.makeUnique('instance'); + return [ + 'if let $instance = $instanceConstruction {', + ' ${wrappedClassInstance.name} = $instance', + '} else {', + ' return nil', + '}', + ]; + } else { + return ['${wrappedClassInstance.name} = $instanceConstruction']; + } +} + +(String, UniqueNamer) _generateInstanceConstructionWithSubset( + InitializerDeclaration originalInitializer, + PropertyDeclaration wrappedClassInstance, + List transformedParams, + List originalParamsSubset, +) { + final localNamer = UniqueNamer(); + final arguments = generateInvocationParams( + localNamer, + originalParamsSubset, + transformedParams, + ); + var instanceConstruction = + '${wrappedClassInstance.type.swiftType}($arguments)'; + if (originalInitializer.async) { + instanceConstruction = 'await $instanceConstruction'; + } + if (originalInitializer.throws) { + instanceConstruction = 'try $instanceConstruction'; + } + return (instanceConstruction, localNamer); +} diff --git a/pkgs/swift2objc/test/integration/default_params_input.swift b/pkgs/swift2objc/test/integration/default_params_input.swift new file mode 100644 index 000000000..2a4b9f807 --- /dev/null +++ b/pkgs/swift2objc/test/integration/default_params_input.swift @@ -0,0 +1,21 @@ +import Foundation + +public class Greeter { + public func greet(name: String = "World", times: Int = 1) -> String { + return String(repeating: "Hello, \(name)! ", count: times) + } + + public init(greeting: String = "Hi") { + self.greeting = greeting + } + + private let greeting: String +} + +public func globalFunc(param: Int = 12) -> Int { + return param * 2 +} + +public func multiDefault(a: Int, b: Int = 10, c: String = "test") -> String { + return "\(a) \(b) \(c)" +} diff --git a/pkgs/swift2objc/test/integration/default_params_output.swift b/pkgs/swift2objc/test/integration/default_params_output.swift new file mode 100644 index 000000000..b0b99dbec --- /dev/null +++ b/pkgs/swift2objc/test/integration/default_params_output.swift @@ -0,0 +1,56 @@ +// Test preamble text + +import Foundation + +@objc public class GlobalsWrapper: NSObject { + @objc static public func globalFuncWrapper(param: Int) -> Int { + return globalFunc(param: param) + } + + @objc static public func globalFuncWrapper() -> Int { + return globalFunc() + } + + @objc static public func multiDefaultWrapper(a: Int, b: Int, c: String) -> String { + return multiDefault(a: a, b: b, c: c) + } + + @objc static public func multiDefaultWrapper(a: Int, b: Int) -> String { + return multiDefault(a: a, b: b) + } + + @objc static public func multiDefaultWrapper(a: Int) -> String { + return multiDefault(a: a) + } + +} + +@objc public class GreeterWrapper: NSObject { + var wrappedInstance: Greeter + + init(_ wrappedInstance: Greeter) { + self.wrappedInstance = wrappedInstance + } + + @objc public init(greeting: String) { + wrappedInstance = Greeter(greeting: greeting) + } + + @objc override public init() { + wrappedInstance = Greeter() + } + + @objc public func greet(name: String, times: Int) -> String { + return wrappedInstance.greet(name: name, times: times) + } + + @objc public func greet(name: String) -> String { + return wrappedInstance.greet(name: name) + } + + @objc public func greet() -> String { + return wrappedInstance.greet() + } + +} + diff --git a/pkgs/swift2objc/test/unit/parse_function_info_test.dart b/pkgs/swift2objc/test/unit/parse_function_info_test.dart index 9ca96c311..8a9fc485d 100644 --- a/pkgs/swift2objc/test/unit/parse_function_info_test.dart +++ b/pkgs/swift2objc/test/unit/parse_function_info_test.dart @@ -35,6 +35,7 @@ void main() { expect(actualParam.name, expectedParam.name); expect(actualParam.internalName, expectedParam.internalName); expect(actualParam.type.sameAs(expectedParam.type), isTrue); + expect(actualParam.defaultValue, expectedParam.defaultValue); } } @@ -515,6 +516,127 @@ void main() { expect(info.throws, isTrue); expect(info.mutating, isTrue); }); + + test('Parameter with default value', () { + final json = Json( + jsonDecode(''' + [ + { "kind": "keyword", "spelling": "func" }, + { "kind": "text", "spelling": " " }, + { "kind": "identifier", "spelling": "foo" }, + { "kind": "text", "spelling": "(" }, + { "kind": "externalParam", "spelling": "param" }, + { "kind": "text", "spelling": ": " }, + { + "kind": "typeIdentifier", + "spelling": "Int", + "preciseIdentifier": "s:Si" + }, + { "kind": "text", "spelling": " = " }, + { "kind": "number", "spelling": "12" }, + { "kind": "text", "spelling": ")" } + ] + '''), + ); + + final info = parseFunctionInfo(context, json, emptySymbolgraph); + + final expectedParams = [ + Parameter(name: 'param', type: intType, defaultValue: '12'), + ]; + + expectEqualParams(info.params, expectedParams); + expect(info.throws, isFalse); + expect(info.async, isFalse); + }); + + test('Multiple parameters with trailing defaults', () { + final json = Json( + jsonDecode(''' + [ + { "kind": "keyword", "spelling": "func" }, + { "kind": "text", "spelling": " " }, + { "kind": "identifier", "spelling": "bar" }, + { "kind": "text", "spelling": "(" }, + { "kind": "externalParam", "spelling": "a" }, + { "kind": "text", "spelling": ": " }, + { + "kind": "typeIdentifier", + "spelling": "Int", + "preciseIdentifier": "s:Si" + }, + { "kind": "text", "spelling": ", " }, + { "kind": "externalParam", "spelling": "b" }, + { "kind": "text", "spelling": ": " }, + { + "kind": "typeIdentifier", + "spelling": "Int", + "preciseIdentifier": "s:Si" + }, + { "kind": "text", "spelling": " = " }, + { "kind": "number", "spelling": "10" }, + { "kind": "text", "spelling": ", " }, + { "kind": "externalParam", "spelling": "c" }, + { "kind": "text", "spelling": ": " }, + { + "kind": "typeIdentifier", + "spelling": "String", + "preciseIdentifier": "s:SS" + }, + { "kind": "text", "spelling": " = " }, + { "kind": "string", "spelling": "\\"hello\\"" }, + { "kind": "text", "spelling": ")" } + ] + '''), + ); + + final info = parseFunctionInfo(context, json, emptySymbolgraph); + + expect(info.params.length, 3); + expect(info.params[0].name, 'a'); + expect(info.params[0].defaultValue, isNull); + expect(info.params[1].name, 'b'); + expect(info.params[1].defaultValue, '10'); + expect(info.params[2].name, 'c'); + expect(info.params[2].defaultValue, '\"hello\"'); + }); + + test('Parameter with string default value', () { + final json = Json( + jsonDecode(''' + [ + { "kind": "keyword", "spelling": "func" }, + { "kind": "text", "spelling": " " }, + { "kind": "identifier", "spelling": "greet" }, + { "kind": "text", "spelling": "(" }, + { "kind": "externalParam", "spelling": "name" }, + { "kind": "text", "spelling": ": " }, + { + "kind": "typeIdentifier", + "spelling": "String", + "preciseIdentifier": "s:SS" + }, + { "kind": "text", "spelling": " = " }, + { "kind": "string", "spelling": "\\"World\\"" }, + { "kind": "text", "spelling": ")" } + ] + '''), + ); + + final info = parseFunctionInfo(context, json, emptySymbolgraph); + + final expectedParams = [ + Parameter( + name: 'name', + type: stringType, + defaultValue: '\"World\"', + ), + ]; + + expectEqualParams(info.params, expectedParams); + expect(info.throws, isFalse); + expect(info.async, isFalse); + }); }); group('Invalid json', () { From fa5929b41a0990f99679358cf861bcc2108bc047 Mon Sep 17 00:00:00 2001 From: Divya Prakash Date: Tue, 30 Dec 2025 22:24:17 +0530 Subject: [PATCH 2/3] Refactor default parameter support to address all review feedback --- .../lib/src/ast/_core/shared/parameter.dart | 4 +- .../lib/src/parser/_core/token_list.dart | 66 ++++- .../parse_function_declaration.dart | 27 +- .../transformers/transform_compound.dart | 34 +-- .../transformers/transform_function.dart | 265 +++++++++--------- .../transformers/transform_globals.dart | 4 +- .../transformers/transform_initializer.dart | 223 ++++++++------- .../test/unit/parse_function_info_test.dart | 169 ++++++++++- .../swift2objc/test/unit/token_list_test.dart | 4 +- 9 files changed, 500 insertions(+), 296 deletions(-) diff --git a/pkgs/swift2objc/lib/src/ast/_core/shared/parameter.dart b/pkgs/swift2objc/lib/src/ast/_core/shared/parameter.dart index c164b437e..0c70edf47 100644 --- a/pkgs/swift2objc/lib/src/ast/_core/shared/parameter.dart +++ b/pkgs/swift2objc/lib/src/ast/_core/shared/parameter.dart @@ -20,7 +20,9 @@ class Parameter extends AstNode { }); @override - String toString() => '$name $internalName: $type${defaultValue != null ? ' = $defaultValue' : ''}'; + String toString() => + '$name $internalName: $type' + '${defaultValue != null ? ' = $defaultValue' : ''}'; @override void visitChildren(Visitor visitor) { diff --git a/pkgs/swift2objc/lib/src/parser/_core/token_list.dart b/pkgs/swift2objc/lib/src/parser/_core/token_list.dart index 3d1be3871..13bda7529 100644 --- a/pkgs/swift2objc/lib/src/parser/_core/token_list.dart +++ b/pkgs/swift2objc/lib/src/parser/_core/token_list.dart @@ -30,42 +30,78 @@ class TokenList extends Iterable { return TokenList._(list, 0, list.length); } + /// Splits a single token on splittable characters, handling both prefix and + /// suffix cases. + /// + /// Swift's symbol graph concatenates some tokens together, for example when + /// a parameter has a default value: " = value) -> returnType" becomes a + /// single text token. This method splits such tokens by: + /// 1. Repeatedly extracting splittables from the start + /// 2. Extracting any splittables from the end (yielding in reverse order) + /// 3. Returning the remaining content as a single text token + /// + /// This approach preserves the correct token sequence and ensures that even + /// complex cases like " = foo) -> " are properly tokenized. @visibleForTesting static Iterable splitToken(Json token) sync* { - const splittables = ['(', ')', '?', ',', '->']; + const splittables = ['(', ')', '?', ',', '->', '=']; Json textToken(String text) => Json({'kind': 'text', 'spelling': text}, token.pathSegments); final text = getSpellingForKind(token, 'text')?.trim(); if (text == null) { - // Not a text token. Pass it though unchanged. + // Not a text token. Pass it through unchanged. yield token; return; } if (text.isEmpty) { - // Input text token was nothing but whitespace. The loop below would yield - // nothing, but we still need it as a separator. + // Input text token was nothing but whitespace. We still need it as a + // separator for the parser. yield textToken(text); return; } - var suffix = text; + var remaining = text; while (true) { - var any = false; - for (final prefix in splittables) { - if (suffix.startsWith(prefix)) { - yield textToken(prefix); - suffix = suffix.substring(prefix.length).trim(); - any = true; + var foundPrefix = false; + for (final splittable in splittables) { + if (remaining.startsWith(splittable)) { + yield textToken(splittable); + remaining = remaining.substring(splittable.length).trim(); + foundPrefix = true; break; } } - if (!any) { - // Remaining text isn't splittable. - if (suffix.isNotEmpty) yield textToken(suffix); - break; + + if (foundPrefix) continue; + + // No more prefix splittables found; extract any trailing ones. + // We collect them in a list and yield in reverse order to maintain + // the original token sequence. + final trailingTokens = []; + var currentSuffix = remaining; + while (currentSuffix.isNotEmpty) { + var foundSuffix = false; + for (final splittable in splittables) { + if (currentSuffix.endsWith(splittable)) { + trailingTokens.add(splittable); + currentSuffix = currentSuffix + .substring(0, currentSuffix.length - splittable.length) + .trim(); + foundSuffix = true; + break; + } + } + if (!foundSuffix) break; + } + + // Yield the core content, then trailing splittables in reverse order. + if (currentSuffix.isNotEmpty) yield textToken(currentSuffix); + for (var i = trailingTokens.length - 1; i >= 0; i--) { + yield textToken(trailingTokens[i]); } + break; } } diff --git a/pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_function_declaration.dart b/pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_function_declaration.dart index ca34b959c..800531b86 100644 --- a/pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_function_declaration.dart +++ b/pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_function_declaration.dart @@ -149,29 +149,29 @@ ParsedFunctionInfo parseFunctionInfo( // Optional: parse a default argument if present. // Swift symbol graph can emit defaults in these formats: // 1. Separate tokens: [": ", type..., " = ", value, ", "] - // 2. Combined: [": ", type..., " = value, "] or [": ", type..., " = value)"] - // 3. With return: [": ", type..., " = value) -> "] + // 2. Combined: [":", type..., " = value, "] or + // [":", type..., " = value)"] + // 3. With return: [":", type..., " = value) -> "] + // + // We carefully preserve string literals during parsing to avoid + // splitting on delimiters that appear inside quoted strings + // (e.g., the ") -> " in the string literal "World ) -> "). String? defaultValue; String? endToken; var afterType = maybeConsume('text'); - + if (afterType != null) { // Check if this text token contains '=' var remaining = afterType.trim(); if (remaining.startsWith('=')) { // Extract default value from this token remaining = remaining.substring(1).trim(); - + // Check for delimiters: ',' or ')' (possibly followed by ' ->') if (remaining.contains(')')) { final parenIndex = remaining.indexOf(')'); defaultValue = remaining.substring(0, parenIndex).trim(); endToken = ')'; - // If there's content after ), put it back as a token - final afterParen = remaining.substring(parenIndex + 1).trim(); - if (afterParen.isNotEmpty) { - // We consumed too much; this is OK, the parser will handle it - } } else if (remaining.contains(',')) { final commaIndex = remaining.indexOf(','); defaultValue = remaining.substring(0, commaIndex).trim(); @@ -183,12 +183,17 @@ ParsedFunctionInfo parseFunctionInfo( } else { // '=' alone, collect tokens until delimiter final parts = []; - while (!tokens.isEmpty) { + while (tokens.isNotEmpty) { final tok = tokens[0]; + final kind = tok['kind'].get(); final spelling = tok['spelling'].get(); if (spelling != null) { final s = spelling.trim(); - if (s == ',' || s == ')') { + // String tokens should be kept whole, not split on delimiters + if (kind == 'string') { + parts.add(spelling); + tokens = tokens.slice(1); + } else if (s == ',' || s == ')') { endToken = s; tokens = tokens.slice(1); break; diff --git a/pkgs/swift2objc/lib/src/transformer/transformers/transform_compound.dart b/pkgs/swift2objc/lib/src/transformer/transformers/transform_compound.dart index 7ec9bd0ad..6508fd08e 100644 --- a/pkgs/swift2objc/lib/src/transformer/transformers/transform_compound.dart +++ b/pkgs/swift2objc/lib/src/transformer/transformers/transform_compound.dart @@ -83,44 +83,26 @@ ClassDeclaration transformCompound( final transformedInitializers = []; for (final init in originalCompound.initializers) { - final main = transformInitializer( - init, - wrappedCompoundInstance, - parentNamer, - state, - ); - transformedInitializers.add(main); transformedInitializers.addAll( - buildDefaultOverloadsForInitializer( + transformInitializerWithOverloads( init, wrappedCompoundInstance, parentNamer, state, - baseTransformed: main, ), ); } final transformedMethods = []; for (final method in originalCompound.methods) { - final main = transformMethod( - method, - wrappedCompoundInstance, - parentNamer, - state, + transformedMethods.addAll( + transformMethodWithOverloads( + method, + wrappedCompoundInstance, + parentNamer, + state, + ), ); - if (main != null) { - transformedMethods.add(main); - transformedMethods.addAll( - buildDefaultOverloadsForMethod( - method, - wrappedCompoundInstance, - parentNamer, - state, - baseTransformed: main, - ), - ); - } } transformedCompound.properties = diff --git a/pkgs/swift2objc/lib/src/transformer/transformers/transform_function.dart b/pkgs/swift2objc/lib/src/transformer/transformers/transform_function.dart index 557acbaaf..4866eff41 100644 --- a/pkgs/swift2objc/lib/src/transformer/transformers/transform_function.dart +++ b/pkgs/swift2objc/lib/src/transformer/transformers/transform_function.dart @@ -8,17 +8,21 @@ import '../../ast/_core/shared/referred_type.dart'; import '../../ast/declarations/compounds/members/method_declaration.dart'; import '../../ast/declarations/compounds/members/property_declaration.dart'; import '../../ast/declarations/globals/globals.dart'; +import '../../parser/_core/utils.dart'; import '../_core/unique_namer.dart'; import '../_core/utils.dart'; import '../transform.dart'; import 'const.dart'; import 'transform_referred_type.dart'; -// The main difference between generating a wrapper method for a global function -// and a compound method is the way the original function/method is referenced. -// In compound method case, the original method is referenced through the -// wrapped class instance in the wrapper class. In global function case, -// it can be referenced directly since it's not a member of any entity. +/// Wrapper generation strategy: For both methods and global functions, we +/// create a primary wrapper with all parameters, then additional overloads +/// that omit trailing parameters with default values. This allows ObjC +/// callers to avoid passing default arguments explicitly. +/// +/// The key difference: +/// - Methods reference the original through a wrapped class instance +/// - Global functions reference the original directly MethodDeclaration? transformMethod( MethodDeclaration originalMethod, @@ -30,6 +34,32 @@ MethodDeclaration? transformMethod( return null; } + final methods = _transformFunction( + originalMethod, + globalNamer, + state, + wrapperMethodName: originalMethod.name, + originalCallStatementGenerator: (arguments) { + final methodSource = originalMethod.isStatic + ? wrappedClassInstance.type.swiftType + : wrappedClassInstance.name; + return '$methodSource.${originalMethod.name}($arguments)'; + }, + ); + + return methods.isEmpty ? null : methods.first; +} + +List transformMethodWithOverloads( + MethodDeclaration originalMethod, + PropertyDeclaration wrappedClassInstance, + UniqueNamer globalNamer, + TransformationState state, +) { + if (disallowedMethods.contains(originalMethod.name)) { + return const []; + } + return _transformFunction( originalMethod, globalNamer, @@ -48,6 +78,22 @@ MethodDeclaration transformGlobalFunction( GlobalFunctionDeclaration globalFunction, UniqueNamer globalNamer, TransformationState state, +) { + final methods = _transformFunction( + globalFunction, + globalNamer, + state, + wrapperMethodName: globalNamer.makeUnique('${globalFunction.name}Wrapper'), + originalCallStatementGenerator: (arguments) => + '${globalFunction.name}($arguments)', + ); + return methods.first; +} + +List transformGlobalFunctionWithOverloads( + GlobalFunctionDeclaration globalFunction, + UniqueNamer globalNamer, + TransformationState state, ) { return _transformFunction( globalFunction, @@ -59,9 +105,33 @@ MethodDeclaration transformGlobalFunction( ); } -// -------------------------- Core Implementation -------------------------- +/// Counts the number of trailing parameters with default values. +/// +/// ObjC doesn't support default parameters, so we generate overloads +/// omitting each combination of trailing defaults. For example, a function +/// with signature `foo(a, b=1, c=2)` generates overloads for `foo(a, b)` +/// and `foo(a)`. +int _trailingDefaultCount(List params) { + var count = 0; + for (var i = params.length - 1; i >= 0; --i) { + if (params[i].defaultValue != null) { + count++; + } else { + break; + } + } + return count; +} -MethodDeclaration _transformFunction( +/// Transforms a Swift function/method into one or more ObjC wrapper methods. +/// +/// Returns a list containing: +/// - The primary wrapper with all parameters +/// - Zero or more overloads omitting trailing default parameters +/// +/// This centralized implementation eliminates duplication between method, +/// initializer, and global function transformation paths. +List _transformFunction( FunctionDeclaration originalFunction, UniqueNamer globalNamer, TransformationState state, { @@ -116,7 +186,66 @@ MethodDeclaration _transformFunction( originalCallGenerator: originalCallStatementGenerator, ); - return transformedMethod; + // Generate overloads for trailing default parameters. Each overload omits + // one more trailing default, allowing ObjC callers to use them without + // explicitly passing default arguments. + final trailingDefaults = _trailingDefaultCount(originalFunction.params); + if (trailingDefaults == 0) { + return [transformedMethod]; + } + + final allMethods = [transformedMethod]; + + for ( + var parametersToOmit = 1; + parametersToOmit <= trailingDefaults; + ++parametersToOmit + ) { + final parameterCount = originalFunction.params.length - parametersToOmit; + final overloadParams = transformedMethod.params.sublist(0, parameterCount); + + final overloadNamer = UniqueNamer(); + final overloadResultName = overloadNamer.makeUnique('result'); + final (overloadWrapperResult, _) = maybeWrapValue( + originalFunction.returnType, + overloadResultName, + globalNamer, + state, + shouldWrapPrimitives: originalFunction.throws, + ); + + final overload = MethodDeclaration( + id: originalFunction.id.addIdSuffix('default$parametersToOmit'), + name: wrapperMethodName, + source: originalFunction.source, + availability: originalFunction.availability, + returnType: transformedMethod.returnType, + params: overloadParams, + hasObjCAnnotation: true, + isStatic: originalFunction is MethodDeclaration + ? originalFunction.isStatic + : true, + throws: originalFunction.throws, + async: originalFunction.async, + ); + + overload.statements = _generateStatementsWithParamSubset( + originalFunction, + overload, + globalNamer, + overloadNamer, + overloadResultName, + overloadWrapperResult, + state, + originalCallGenerator: originalCallStatementGenerator, + originalParamsForCall: originalFunction.params.sublist(0, parameterCount), + transformedParamsForCall: overloadParams, + ); + + allMethods.add(overload); + } + + return allMethods; } String generateInvocationParams( @@ -210,123 +339,3 @@ List _generateStatementsWithParamSubset( return ['let $resultName = $originalMethodCall', 'return $wrappedResult']; } - -int _trailingDefaultCount(List params) { - var count = 0; - for (var i = params.length - 1; i >= 0; --i) { - if (params[i].defaultValue != null) { - count++; - } else { - break; - } - } - return count; -} - -List buildDefaultOverloadsForMethod( - MethodDeclaration originalMethod, - PropertyDeclaration wrappedClassInstance, - UniqueNamer globalNamer, - TransformationState state, { - required MethodDeclaration baseTransformed, -}) { - final defaults = _trailingDefaultCount(originalMethod.params); - if (defaults == 0) return const []; - - final overloads = []; - for (var drop = 1; drop <= defaults; ++drop) { - final keep = originalMethod.params.length - drop; - final transformedSubset = baseTransformed.params.sublist(0, keep); - - final over = MethodDeclaration( - id: '${originalMethod.id}-default$drop', - name: baseTransformed.name, - source: originalMethod.source, - availability: originalMethod.availability, - returnType: baseTransformed.returnType, - params: transformedSubset, - hasObjCAnnotation: true, - isStatic: originalMethod.isStatic, - throws: originalMethod.throws, - async: originalMethod.async, - ); - - final localNamer = UniqueNamer(); - final resultName = localNamer.makeUnique('result'); - final (wrapperResult, _) = maybeWrapValue( - originalMethod.returnType, - resultName, - globalNamer, - state, - shouldWrapPrimitives: originalMethod.throws, - ); - final methodSource = originalMethod.isStatic - ? wrappedClassInstance.type.swiftType - : wrappedClassInstance.name; - over.statements = _generateStatementsWithParamSubset( - originalMethod, - over, - globalNamer, - localNamer, - resultName, - wrapperResult, - state, - originalCallGenerator: (args) => '$methodSource.${originalMethod.name}($args)', - originalParamsForCall: originalMethod.params.sublist(0, keep), - transformedParamsForCall: transformedSubset, - ); - overloads.add(over); - } - return overloads; -} - -List buildDefaultOverloadsForGlobalFunction( - GlobalFunctionDeclaration globalFunction, - MethodDeclaration baseTransformed, - UniqueNamer globalNamer, - TransformationState state, -) { - final defaults = _trailingDefaultCount(globalFunction.params); - if (defaults == 0) return const []; - final overloads = []; - for (var drop = 1; drop <= defaults; ++drop) { - final keep = globalFunction.params.length - drop; - final subset = baseTransformed.params.sublist(0, keep); - final over = MethodDeclaration( - id: '${globalFunction.id}-default$drop', - name: baseTransformed.name, - source: globalFunction.source, - availability: globalFunction.availability, - returnType: baseTransformed.returnType, - params: subset, - hasObjCAnnotation: true, - isStatic: true, - throws: globalFunction.throws, - async: globalFunction.async, - ); - - final localNamer = UniqueNamer(); - final resultName = localNamer.makeUnique('result'); - final (wrapperResult, _) = maybeWrapValue( - globalFunction.returnType, - resultName, - globalNamer, - state, - shouldWrapPrimitives: globalFunction.throws, - ); - over.statements = _generateStatementsWithParamSubset( - globalFunction, - over, - globalNamer, - localNamer, - resultName, - wrapperResult, - state, - originalCallGenerator: (args) => '${globalFunction.name}($args)', - originalParamsForCall: globalFunction.params.sublist(0, keep), - transformedParamsForCall: subset, - ); - overloads.add(over); - } - return overloads; -} diff --git a/pkgs/swift2objc/lib/src/transformer/transformers/transform_globals.dart b/pkgs/swift2objc/lib/src/transformer/transformers/transform_globals.dart index e7feeab62..6f3557ca5 100644 --- a/pkgs/swift2objc/lib/src/transformer/transformers/transform_globals.dart +++ b/pkgs/swift2objc/lib/src/transformer/transformers/transform_globals.dart @@ -37,10 +37,8 @@ ClassDeclaration? transformGlobals( final transformedMethods = []; for (final fn in globals.functions) { - final main = transformGlobalFunction(fn, globalNamer, state); - transformedMethods.add(main); transformedMethods.addAll( - buildDefaultOverloadsForGlobalFunction(fn, main, globalNamer, state), + transformGlobalFunctionWithOverloads(fn, globalNamer, state), ); } diff --git a/pkgs/swift2objc/lib/src/transformer/transformers/transform_initializer.dart b/pkgs/swift2objc/lib/src/transformer/transformers/transform_initializer.dart index fe32c67fc..26ef758a1 100644 --- a/pkgs/swift2objc/lib/src/transformer/transformers/transform_initializer.dart +++ b/pkgs/swift2objc/lib/src/transformer/transformers/transform_initializer.dart @@ -8,6 +8,7 @@ import '../../ast/_core/shared/referred_type.dart'; import '../../ast/declarations/compounds/members/initializer_declaration.dart'; import '../../ast/declarations/compounds/members/method_declaration.dart'; import '../../ast/declarations/compounds/members/property_declaration.dart'; +import '../../parser/_core/utils.dart'; import '../_core/unique_namer.dart'; import '../transform.dart'; import 'transform_function.dart'; @@ -18,6 +19,21 @@ Declaration transformInitializer( PropertyDeclaration wrappedClassInstance, UniqueNamer globalNamer, TransformationState state, +) { + final declarations = transformInitializerWithOverloads( + originalInitializer, + wrappedClassInstance, + globalNamer, + state, + ); + return declarations.first; +} + +List transformInitializerWithOverloads( + InitializerDeclaration originalInitializer, + PropertyDeclaration wrappedClassInstance, + UniqueNamer globalNamer, + TransformationState state, ) { final transformedParams = originalInitializer.params .map( @@ -25,10 +41,13 @@ Declaration transformInitializer( name: param.name, internalName: param.internalName, type: transformReferredType(param.type, globalNamer, state), + defaultValue: param.defaultValue, ), ) .toList(); + final Declaration mainDeclaration; + if (originalInitializer.async) { final methodReturnType = transformReferredType( wrappedClassInstance.type, @@ -36,7 +55,7 @@ Declaration transformInitializer( state, ); - return MethodDeclaration( + mainDeclaration = MethodDeclaration( id: originalInitializer.id, name: '${originalInitializer.name}Wrapper', source: originalInitializer.source, @@ -56,30 +75,105 @@ Declaration transformInitializer( async: originalInitializer.async, isStatic: true, ); + } else { + final transformedInitializer = InitializerDeclaration( + id: originalInitializer.id, + source: originalInitializer.source, + availability: originalInitializer.availability, + params: transformedParams, + hasObjCAnnotation: true, + isFailable: originalInitializer.isFailable, + throws: originalInitializer.throws, + async: originalInitializer.async, + isOverriding: transformedParams.isEmpty, + ); + + transformedInitializer.statements = _generateInitializerStatements( + originalInitializer, + wrappedClassInstance, + transformedInitializer, + ); + + mainDeclaration = transformedInitializer; } - final transformedInitializer = InitializerDeclaration( - id: originalInitializer.id, - source: originalInitializer.source, - availability: originalInitializer.availability, - params: transformedParams, - hasObjCAnnotation: true, - isFailable: originalInitializer.isFailable, - throws: originalInitializer.throws, - async: originalInitializer.async, - // Because the wrapper class extends NSObject that has an initializer with - // no parameters. If we make a similar parameterless initializer we need - // to add `override` keyword. - isOverriding: transformedParams.isEmpty, - ); + // Generate overloads for trailing default parameters + final defaults = _trailingDefaultCount(originalInitializer.params); + if (defaults == 0) { + return [mainDeclaration]; + } - transformedInitializer.statements = _generateInitializerStatements( - originalInitializer, - wrappedClassInstance, - transformedInitializer, - ); + final declarations = [mainDeclaration]; + + for (var drop = 1; drop <= defaults; ++drop) { + final keep = originalInitializer.params.length - drop; + final transformedSubsetParams = originalInitializer.params + .sublist(0, keep) + .map( + (param) => Parameter( + name: param.name, + internalName: param.internalName, + type: transformReferredType(param.type, globalNamer, state), + defaultValue: param.defaultValue, + ), + ) + .toList(); + + if (originalInitializer.async) { + // Generate async method wrapper overload + final methodReturnType = transformReferredType( + wrappedClassInstance.type, + globalNamer, + state, + ); + + final over = MethodDeclaration( + id: originalInitializer.id.addIdSuffix('default$drop'), + name: '${originalInitializer.name}Wrapper', + source: originalInitializer.source, + availability: originalInitializer.availability, + returnType: originalInitializer.isFailable + ? OptionalType(methodReturnType) + : methodReturnType, + params: transformedSubsetParams, + hasObjCAnnotation: true, + throws: originalInitializer.throws, + async: originalInitializer.async, + isStatic: true, + ); + + over.statements = _generateMethodStatements( + originalInitializer, + wrappedClassInstance, + methodReturnType, + transformedSubsetParams, + ); + declarations.add(over); + } else { + // Generate regular initializer overload + final over = InitializerDeclaration( + id: originalInitializer.id.addIdSuffix('default$drop'), + source: originalInitializer.source, + availability: originalInitializer.availability, + params: transformedSubsetParams, + hasObjCAnnotation: true, + isFailable: originalInitializer.isFailable, + throws: originalInitializer.throws, + async: originalInitializer.async, + isOverriding: transformedSubsetParams.isEmpty, + ); + + over.statements = _generateInitializerStatementsWithSubset( + originalInitializer, + wrappedClassInstance, + over, + originalInitializer.params.sublist(0, keep), + ); + declarations.add(over); + } + } - return transformedInitializer; + return declarations; } List _generateInitializerStatements( @@ -168,95 +262,16 @@ int _trailingDefaultCount(List params) { return count; } -List buildDefaultOverloadsForInitializer( - InitializerDeclaration originalInitializer, - PropertyDeclaration wrappedClassInstance, - UniqueNamer globalNamer, - TransformationState state, { - required Declaration baseTransformed, -}) { - final defaults = _trailingDefaultCount(originalInitializer.params); - if (defaults == 0) return const []; - - final overloads = []; - for (var drop = 1; drop <= defaults; ++drop) { - final keep = originalInitializer.params.length - drop; - final transformedSubsetParams = originalInitializer.params - .sublist(0, keep) - .map( - (param) => Parameter( - name: param.name, - internalName: param.internalName, - type: transformReferredType(param.type, globalNamer, state), - defaultValue: param.defaultValue, - ), - ) - .toList(); - - if (originalInitializer.async) { - // Generate async method wrapper overload - final methodReturnType = transformReferredType( - wrappedClassInstance.type, - globalNamer, - state, - ); - - final over = MethodDeclaration( - id: '${originalInitializer.id}-default$drop', - name: '${originalInitializer.name}Wrapper', - source: originalInitializer.source, - availability: originalInitializer.availability, - returnType: originalInitializer.isFailable - ? OptionalType(methodReturnType) - : methodReturnType, - params: transformedSubsetParams, - hasObjCAnnotation: true, - throws: originalInitializer.throws, - async: originalInitializer.async, - isStatic: true, - ); - - over.statements = _generateMethodStatements( - originalInitializer, - wrappedClassInstance, - methodReturnType, - transformedSubsetParams, - ); - overloads.add(over); - } else { - // Generate regular initializer overload - final over = InitializerDeclaration( - id: '${originalInitializer.id}-default$drop', - source: originalInitializer.source, - availability: originalInitializer.availability, - params: transformedSubsetParams, - hasObjCAnnotation: true, - isFailable: originalInitializer.isFailable, - throws: originalInitializer.throws, - async: originalInitializer.async, - isOverriding: transformedSubsetParams.isEmpty, - ); - - over.statements = _generateInitializerStatementsWithSubset( - originalInitializer, - wrappedClassInstance, - over, - originalInitializer.params.sublist(0, keep), - ); - overloads.add(over); - } - } - return overloads; -} - List _generateInitializerStatementsWithSubset( InitializerDeclaration originalInitializer, PropertyDeclaration wrappedClassInstance, InitializerDeclaration transformedInitializer, List originalParamsSubset, ) { - final (instanceConstruction, localNamer) = - _generateInstanceConstructionWithSubset( + final ( + instanceConstruction, + localNamer, + ) = _generateInstanceConstructionWithSubset( originalInitializer, wrappedClassInstance, transformedInitializer.params, diff --git a/pkgs/swift2objc/test/unit/parse_function_info_test.dart b/pkgs/swift2objc/test/unit/parse_function_info_test.dart index 8a9fc485d..75d7ba76c 100644 --- a/pkgs/swift2objc/test/unit/parse_function_info_test.dart +++ b/pkgs/swift2objc/test/unit/parse_function_info_test.dart @@ -598,7 +598,7 @@ void main() { expect(info.params[1].name, 'b'); expect(info.params[1].defaultValue, '10'); expect(info.params[2].name, 'c'); - expect(info.params[2].defaultValue, '\"hello\"'); + expect(info.params[2].defaultValue, '"hello"'); }); test('Parameter with string default value', () { @@ -626,17 +626,174 @@ void main() { final info = parseFunctionInfo(context, json, emptySymbolgraph); final expectedParams = [ - Parameter( - name: 'name', - type: stringType, - defaultValue: '\"World\"', - ), + Parameter(name: 'name', type: stringType, defaultValue: '"World"'), ]; expectEqualParams(info.params, expectedParams); expect(info.throws, isFalse); expect(info.async, isFalse); }); + + test('Parameter with non-trivial expression default', () { + final json = Json( + jsonDecode(''' + [ + { "kind": "keyword", "spelling": "func" }, + { "kind": "text", "spelling": " " }, + { "kind": "identifier", "spelling": "compute" }, + { "kind": "text", "spelling": "(" }, + { "kind": "externalParam", "spelling": "value" }, + { "kind": "text", "spelling": ": " }, + { + "kind": "typeIdentifier", + "spelling": "Int", + "preciseIdentifier": "s:Si" + }, + { "kind": "text", "spelling": " = " }, + { "kind": "number", "spelling": "10" }, + { "kind": "text", "spelling": " * foo)" } + ] + '''), + ); + + final info = parseFunctionInfo(context, json, emptySymbolgraph); + + expect(info.params.length, 1); + expect(info.params[0].name, 'value'); + expect(info.params[0].defaultValue, '10* foo'); + }); + + test('Default value in separate tokens', () { + final json = Json( + jsonDecode(''' + [ + { "kind": "keyword", "spelling": "func" }, + { "kind": "text", "spelling": " " }, + { "kind": "identifier", "spelling": "test" }, + { "kind": "text", "spelling": "(" }, + { "kind": "externalParam", "spelling": "x" }, + { "kind": "text", "spelling": ": " }, + { + "kind": "typeIdentifier", + "spelling": "Int", + "preciseIdentifier": "s:Si" + }, + { "kind": "text", "spelling": " = " }, + { "kind": "number", "spelling": "42" }, + { "kind": "text", "spelling": ")" } + ] + '''), + ); + + final info = parseFunctionInfo(context, json, emptySymbolgraph); + + expect(info.params.length, 1); + expect(info.params[0].name, 'x'); + expect(info.params[0].defaultValue, '42'); + }); + + test('Default value combined with comma', () { + final json = Json( + jsonDecode(''' + [ + { "kind": "keyword", "spelling": "func" }, + { "kind": "text", "spelling": " " }, + { "kind": "identifier", "spelling": "test" }, + { "kind": "text", "spelling": "(" }, + { "kind": "externalParam", "spelling": "x" }, + { "kind": "text", "spelling": ": " }, + { + "kind": "typeIdentifier", + "spelling": "Int", + "preciseIdentifier": "s:Si" + }, + { "kind": "text", "spelling": " = 42, " }, + { "kind": "externalParam", "spelling": "y" }, + { "kind": "text", "spelling": ": " }, + { + "kind": "typeIdentifier", + "spelling": "Int", + "preciseIdentifier": "s:Si" + }, + { "kind": "text", "spelling": ")" } + ] + '''), + ); + + final info = parseFunctionInfo(context, json, emptySymbolgraph); + + expect(info.params.length, 2); + expect(info.params[0].name, 'x'); + expect(info.params[0].defaultValue, '42'); + expect(info.params[1].name, 'y'); + expect(info.params[1].defaultValue, isNull); + }); + + test('Default value with return type arrow', () { + final json = Json( + jsonDecode(''' + [ + { "kind": "keyword", "spelling": "func" }, + { "kind": "text", "spelling": " " }, + { "kind": "identifier", "spelling": "test" }, + { "kind": "text", "spelling": "(" }, + { "kind": "externalParam", "spelling": "x" }, + { "kind": "text", "spelling": ": " }, + { + "kind": "typeIdentifier", + "spelling": "Int", + "preciseIdentifier": "s:Si" + }, + { "kind": "text", "spelling": " = 99) -> " }, + { + "kind": "typeIdentifier", + "spelling": "Int", + "preciseIdentifier": "s:Si" + } + ] + '''), + ); + + final info = parseFunctionInfo(context, json, emptySymbolgraph); + + expect(info.params.length, 1); + expect(info.params[0].name, 'x'); + expect(info.params[0].defaultValue, '99'); + }); + + test('Evil string with arrow inside default value', () { + final json = Json( + jsonDecode(''' + [ + { "kind": "keyword", "spelling": "func" }, + { "kind": "text", "spelling": " " }, + { "kind": "identifier", "spelling": "greet" }, + { "kind": "text", "spelling": "(" }, + { "kind": "externalParam", "spelling": "name" }, + { "kind": "text", "spelling": ": " }, + { + "kind": "typeIdentifier", + "spelling": "String", + "preciseIdentifier": "s:SS" + }, + { "kind": "text", "spelling": " = " }, + { "kind": "string", "spelling": "\\"World ) -> \\"" }, + { "kind": "text", "spelling": ") -> " }, + { + "kind": "typeIdentifier", + "spelling": "String", + "preciseIdentifier": "s:SS" + } + ] + '''), + ); + + final info = parseFunctionInfo(context, json, emptySymbolgraph); + + expect(info.params.length, 1); + expect(info.params[0].name, 'name'); + expect(info.params[0].defaultValue, '"World ) -> "'); + }); }); group('Invalid json', () { diff --git a/pkgs/swift2objc/test/unit/token_list_test.dart b/pkgs/swift2objc/test/unit/token_list_test.dart index aba987c8f..f13b6c8f6 100644 --- a/pkgs/swift2objc/test/unit/token_list_test.dart +++ b/pkgs/swift2objc/test/unit/token_list_test.dart @@ -85,11 +85,11 @@ void main() { // point), and we haven't seen a symbolgraph where that's necessary yet. expect( spelling(split('{ "kind": "text", "spelling": "?)>-??" }')), - '["?", ")", ">-??"]', + '["?", ")", ">-", "?", "?"]', ); expect( spelling(split('{ "kind": "text", "spelling": "?)abc??" }')), - '["?", ")", "abc??"]', + '["?", ")", "abc", "?", "?"]', ); }); From 26230564eeda1f22bd04f176b80c88daefa7040f Mon Sep 17 00:00:00 2001 From: Divya Prakash Date: Tue, 30 Dec 2025 22:39:54 +0530 Subject: [PATCH 3/3] Refactor: Finalize token splitting logic for default parameters --- pkgs/swift2objc/lib/src/parser/_core/token_list.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkgs/swift2objc/lib/src/parser/_core/token_list.dart b/pkgs/swift2objc/lib/src/parser/_core/token_list.dart index 13bda7529..bb39787fb 100644 --- a/pkgs/swift2objc/lib/src/parser/_core/token_list.dart +++ b/pkgs/swift2objc/lib/src/parser/_core/token_list.dart @@ -37,8 +37,9 @@ class TokenList extends Iterable { /// a parameter has a default value: " = value) -> returnType" becomes a /// single text token. This method splits such tokens by: /// 1. Repeatedly extracting splittables from the start - /// 2. Extracting any splittables from the end (yielding in reverse order) - /// 3. Returning the remaining content as a single text token + /// 2. For the remaining content, pull splittables from the end + /// 3. Store removed suffix tokens and yield them in reverse order + /// 4. Yield any remaining non-splittable content as a single text token /// /// This approach preserves the correct token sequence and ensures that even /// complex cases like " = foo) -> " are properly tokenized.