diff --git a/pkgs/swift2objc/lib/src/ast/_core/shared/parameter.dart b/pkgs/swift2objc/lib/src/ast/_core/shared/parameter.dart index 4e1a1ebb0..0c70edf47 100644 --- a/pkgs/swift2objc/lib/src/ast/_core/shared/parameter.dart +++ b/pkgs/swift2objc/lib/src/ast/_core/shared/parameter.dart @@ -10,11 +10,19 @@ 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/_core/token_list.dart b/pkgs/swift2objc/lib/src/parser/_core/token_list.dart index 3d1be3871..bb39787fb 100644 --- a/pkgs/swift2objc/lib/src/parser/_core/token_list.dart +++ b/pkgs/swift2objc/lib/src/parser/_core/token_list.dart @@ -30,42 +30,79 @@ 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. 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. @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 aedfc19f0..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 @@ -146,11 +146,91 @@ 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) -> "] + // + // 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 = ')'; + } 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.isNotEmpty) { + final tok = tokens[0]; + final kind = tok['kind'].get(); + final spelling = tok['spelling'].get(); + if (spelling != null) { + final s = spelling.trim(); + // 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; + } 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..6508fd08e 100644 --- a/pkgs/swift2objc/lib/src/transformer/transformers/transform_compound.dart +++ b/pkgs/swift2objc/lib/src/transformer/transformers/transform_compound.dart @@ -81,28 +81,29 @@ ClassDeclaration transformCompound( .nonNulls .toList(); - final transformedInitializers = originalCompound.initializers - .map( - (initializer) => transformInitializer( - initializer, - wrappedCompoundInstance, - parentNamer, - state, - ), - ) - .toList(); + final transformedInitializers = []; + for (final init in originalCompound.initializers) { + transformedInitializers.addAll( + transformInitializerWithOverloads( + init, + wrappedCompoundInstance, + parentNamer, + state, + ), + ); + } - final transformedMethods = originalCompound.methods - .map( - (method) => transformMethod( - method, - wrappedCompoundInstance, - parentNamer, - state, - ), - ) - .nonNulls - .toList(); + final transformedMethods = []; + for (final method in originalCompound.methods) { + transformedMethods.addAll( + transformMethodWithOverloads( + method, + wrappedCompoundInstance, + parentNamer, + state, + ), + ); + } 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..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, { @@ -74,6 +144,7 @@ MethodDeclaration _transformFunction( name: param.name, internalName: param.internalName, type: transformReferredType(param.type, globalNamer, state), + defaultValue: param.defaultValue, ), ) .toList(); @@ -115,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( @@ -159,11 +289,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) { diff --git a/pkgs/swift2objc/lib/src/transformer/transformers/transform_globals.dart b/pkgs/swift2objc/lib/src/transformer/transformers/transform_globals.dart index 67d93a3b2..6f3557ca5 100644 --- a/pkgs/swift2objc/lib/src/transformer/transformers/transform_globals.dart +++ b/pkgs/swift2objc/lib/src/transformer/transformers/transform_globals.dart @@ -35,9 +35,12 @@ 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) { + transformedMethods.addAll( + transformGlobalFunctionWithOverloads(fn, 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..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]; - return transformedInitializer; + 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 declarations; } List _generateInitializerStatements( @@ -155,3 +249,67 @@ 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 _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..75d7ba76c 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,284 @@ 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); + }); + + 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", "?", "?"]', ); });