From b2aca7e77f41b79d7c836971b6747facbfc0adc0 Mon Sep 17 00:00:00 2001 From: Liam Appelbe Date: Wed, 20 Nov 2024 09:04:13 +1100 Subject: [PATCH] [swift2objc] Support failable initializers (#1734) --- .../members/initializer_declaration.dart | 3 + .../generator/generators/class_generator.dart | 8 +- .../swift2objc/lib/src/parser/_core/json.dart | 4 + .../lib/src/parser/_core/utils.dart | 23 ++- .../parse_initializer_declaration.dart | 29 ++-- .../src/transformer/_core/unique_namer.dart | 7 +- .../lib/src/transformer/_core/utils.dart | 4 + .../transformers/transform_compound.dart | 1 + .../transformers/transform_initializer.dart | 19 ++- .../classes_and_initializers_input.swift | 9 ++ .../classes_and_initializers_output.swift | 8 + .../test/integration/integration_test.dart | 5 + .../structs_and_initializers_input.swift | 9 ++ .../structs_and_initializers_output.swift | 8 + .../unit/parse_initializer_param_test.dart | 150 ++++++++---------- 15 files changed, 178 insertions(+), 109 deletions(-) diff --git a/pkgs/swift2objc/lib/src/ast/declarations/compounds/members/initializer_declaration.dart b/pkgs/swift2objc/lib/src/ast/declarations/compounds/members/initializer_declaration.dart index 15c4dd6be..287afbbbf 100644 --- a/pkgs/swift2objc/lib/src/ast/declarations/compounds/members/initializer_declaration.dart +++ b/pkgs/swift2objc/lib/src/ast/declarations/compounds/members/initializer_declaration.dart @@ -29,6 +29,8 @@ class InitializerDeclaration @override bool isOverriding; + bool isFailable; + @override List params; @@ -41,5 +43,6 @@ class InitializerDeclaration this.statements = const [], required this.hasObjCAnnotation, required this.isOverriding, + required this.isFailable, }); } diff --git a/pkgs/swift2objc/lib/src/generator/generators/class_generator.dart b/pkgs/swift2objc/lib/src/generator/generators/class_generator.dart index 56a1bdb6f..9f335e26f 100644 --- a/pkgs/swift2objc/lib/src/generator/generators/class_generator.dart +++ b/pkgs/swift2objc/lib/src/generator/generators/class_generator.dart @@ -68,7 +68,13 @@ List _generateInitializers(ClassDeclaration declaration) { header.write('override '); } - header.write('init(${generateParameters(initializer.params)})'); + header.write('init'); + + if (initializer.isFailable) { + header.write('?'); + } + + header.write('(${generateParameters(initializer.params)})'); return ['$header {', initializer.statements.join('\n').indent(), '}'] .join('\n'); diff --git a/pkgs/swift2objc/lib/src/parser/_core/json.dart b/pkgs/swift2objc/lib/src/parser/_core/json.dart index c2b7b711e..3f8fd5a59 100644 --- a/pkgs/swift2objc/lib/src/parser/_core/json.dart +++ b/pkgs/swift2objc/lib/src/parser/_core/json.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:collection'; +import 'dart:convert'; /// This is a helper class that helps with parsing Json values. It supports /// accessing the json content using the subscript syntax similar to `List` @@ -101,6 +102,9 @@ class Json extends IterableBase { ), ); } + + @override + String toString() => jsonEncode(_json); } class _JsonIterator implements Iterator { diff --git a/pkgs/swift2objc/lib/src/parser/_core/utils.dart b/pkgs/swift2objc/lib/src/parser/_core/utils.dart index 698bfb5bc..9bb0ba3d7 100644 --- a/pkgs/swift2objc/lib/src/parser/_core/utils.dart +++ b/pkgs/swift2objc/lib/src/parser/_core/utils.dart @@ -38,6 +38,11 @@ extension TopLevelOnly on List { ).toList(); } +/// Matches fragments, which look like {"kind": "foo", "spelling": "bar"}. +bool matchFragment(Json fragment, String kind, String spelling) => + fragment['kind'].get() == kind && + fragment['spelling'].get() == spelling; + String parseSymbolId(Json symbolJson) { final idJson = symbolJson['identifier']['precise']; final id = idJson.get(); @@ -56,23 +61,13 @@ String parseSymbolName(Json symbolJson) { } bool parseSymbolHasObjcAnnotation(Json symbolJson) { - return symbolJson['declarationFragments'].any( - (json) => - json['kind'].exists && - json['kind'].get() == 'attribute' && - json['spelling'].exists && - json['spelling'].get() == '@objc', - ); + return symbolJson['declarationFragments'] + .any((json) => matchFragment(json, 'attribute', '@objc')); } bool parseIsOverriding(Json symbolJson) { - return symbolJson['declarationFragments'].any( - (json) => - json['kind'].exists && - json['kind'].get() == 'keyword' && - json['spelling'].exists && - json['spelling'].get() == 'override', - ); + return symbolJson['declarationFragments'] + .any((json) => matchFragment(json, 'keyword', 'override')); } ReferredType parseTypeFromId(String typeId, ParsedSymbolgraph symbolgraph) { diff --git a/pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_initializer_declaration.dart b/pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_initializer_declaration.dart index bbd26f254..d5cb9192f 100644 --- a/pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_initializer_declaration.dart +++ b/pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_initializer_declaration.dart @@ -8,22 +8,33 @@ InitializerDeclaration parseInitializerDeclaration( Json initializerSymbolJson, ParsedSymbolgraph symbolgraph, ) { + final id = parseSymbolId(initializerSymbolJson); + + // Initializers don't have `functionSignature` field in symbolgraph like + // methods do, so we have our only option is to use `declarationFragments`. + final declarationFragments = initializerSymbolJson['declarationFragments']; + + // All initializers should start with an `init` keyword. + if (!matchFragment(declarationFragments[0], 'keyword', 'init')) { + throw Exception('Invalid initializer at ${declarationFragments.path}: $id'); + } + return InitializerDeclaration( - id: parseSymbolId(initializerSymbolJson), - params: parseInitializerParams(initializerSymbolJson, symbolgraph), + id: id, + params: parseInitializerParams(declarationFragments, symbolgraph), hasObjCAnnotation: parseSymbolHasObjcAnnotation(initializerSymbolJson), isOverriding: parseIsOverriding(initializerSymbolJson), + isFailable: parseIsFailableInit(id, declarationFragments), ); } +bool parseIsFailableInit(String id, Json declarationFragments) => + matchFragment(declarationFragments[1], 'text', '?('); + List parseInitializerParams( - Json initializerSymbolJson, + Json declarationFragments, ParsedSymbolgraph symbolgraph, ) { - // Initializers don't have `functionSignature` field in symbolgraph like - // methods do, so we have our only option is to use `declarationFragments`. - final fragments = initializerSymbolJson['declarationFragments']; - // `declarationFragments` describes each part of the initializer declaration, // things like `init` keyword, brackets, spaces, etc. We only care about the // parameter fragments here, and they always appear in this order: @@ -51,7 +62,7 @@ List parseInitializerParams( final parameters = []; - for (final fragmentJson in fragments) { + for (final fragmentJson in declarationFragments) { final kind = fragmentJson['kind'].get(); final invalidOrderException = Exception( 'Invalid fragments order at ${fragmentJson.path}', @@ -94,7 +105,7 @@ List parseInitializerParams( // of `declarationFragments` array. if (externalParam != null || internalParam != null || typeId != null) { throw Exception( - 'Missing parameter fragments at the end of ${fragments.path}', + 'Missing parameter fragments at the end of ${declarationFragments.path}', ); } diff --git a/pkgs/swift2objc/lib/src/transformer/_core/unique_namer.dart b/pkgs/swift2objc/lib/src/transformer/_core/unique_namer.dart index 9af08ae67..8a13e1202 100644 --- a/pkgs/swift2objc/lib/src/transformer/_core/unique_namer.dart +++ b/pkgs/swift2objc/lib/src/transformer/_core/unique_namer.dart @@ -1,9 +1,14 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + import '../../ast/_core/interfaces/compound_declaration.dart'; class UniqueNamer { final Set _usedNames; - UniqueNamer(Iterable usedNames) : _usedNames = usedNames.toSet(); + UniqueNamer([Iterable usedNames = const []]) + : _usedNames = usedNames.toSet(); UniqueNamer.inCompound(CompoundDeclaration compoundDeclaration) : _usedNames = { diff --git a/pkgs/swift2objc/lib/src/transformer/_core/utils.dart b/pkgs/swift2objc/lib/src/transformer/_core/utils.dart index 8561d6a0f..33b99b58d 100644 --- a/pkgs/swift2objc/lib/src/transformer/_core/utils.dart +++ b/pkgs/swift2objc/lib/src/transformer/_core/utils.dart @@ -1,3 +1,7 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + import '../../ast/_core/interfaces/declaration.dart'; import '../../ast/_core/shared/referred_type.dart'; import '../../ast/declarations/compounds/class_declaration.dart'; diff --git a/pkgs/swift2objc/lib/src/transformer/transformers/transform_compound.dart b/pkgs/swift2objc/lib/src/transformer/transformers/transform_compound.dart index 989f530a8..7ccd5f0b0 100644 --- a/pkgs/swift2objc/lib/src/transformer/transformers/transform_compound.dart +++ b/pkgs/swift2objc/lib/src/transformer/transformers/transform_compound.dart @@ -89,6 +89,7 @@ InitializerDeclaration _buildWrapperInitializer( ) ], isOverriding: false, + isFailable: false, statements: ['self.${wrappedClassInstance.name} = wrappedInstance'], hasObjCAnnotation: wrappedClassInstance.hasObjCAnnotation, ); diff --git a/pkgs/swift2objc/lib/src/transformer/transformers/transform_initializer.dart b/pkgs/swift2objc/lib/src/transformer/transformers/transform_initializer.dart index 2d5667041..2fa043edd 100644 --- a/pkgs/swift2objc/lib/src/transformer/transformers/transform_initializer.dart +++ b/pkgs/swift2objc/lib/src/transformer/transformers/transform_initializer.dart @@ -34,6 +34,7 @@ InitializerDeclaration transformInitializer( id: originalInitializer.id, params: transformedParams, hasObjCAnnotation: true, + isFailable: originalInitializer.isFailable, // 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. @@ -54,13 +55,14 @@ List _generateInitializerStatements( InitializerDeclaration transformedInitializer, ) { final argumentsList = []; + final localNamer = UniqueNamer(); for (var i = 0; i < originalInitializer.params.length; i++) { final originalParam = originalInitializer.params[i]; final transformedParam = transformedInitializer.params[i]; - final transformedParamName = - transformedParam.internalName ?? transformedParam.name; + final transformedParamName = localNamer + .makeUnique(transformedParam.internalName ?? transformedParam.name); final (unwrappedParamValue, unwrappedType) = maybeUnwrapValue( transformedParam.type, @@ -77,5 +79,16 @@ List _generateInitializerStatements( final arguments = argumentsList.join(', '); final instanceConstruction = '${wrappedClassInstance.type.name}($arguments)'; - return ['${wrappedClassInstance.name} = $instanceConstruction']; + if (originalInitializer.isFailable) { + final instance = localNamer.makeUnique('instance'); + return [ + 'if let $instance = $instanceConstruction {', + ' ${wrappedClassInstance.name} = $instance', + '} else {', + ' return nil', + '}', + ]; + } else { + return ['${wrappedClassInstance.name} = $instanceConstruction']; + } } diff --git a/pkgs/swift2objc/test/integration/classes_and_initializers_input.swift b/pkgs/swift2objc/test/integration/classes_and_initializers_input.swift index e8ce0fbab..46977f5ab 100644 --- a/pkgs/swift2objc/test/integration/classes_and_initializers_input.swift +++ b/pkgs/swift2objc/test/integration/classes_and_initializers_input.swift @@ -8,6 +8,15 @@ public class MyClass { self.representableProperty = representableProperty self.customProperty = customProperty } + + public init?(outerLabel x: Int) { + if x == 0 { + return nil + } else { + self.representableProperty = x + self.customProperty = MyOtherClass() + } + } } public class MyOtherClass {} diff --git a/pkgs/swift2objc/test/integration/classes_and_initializers_output.swift b/pkgs/swift2objc/test/integration/classes_and_initializers_output.swift index 7d4604b22..235624df8 100644 --- a/pkgs/swift2objc/test/integration/classes_and_initializers_output.swift +++ b/pkgs/swift2objc/test/integration/classes_and_initializers_output.swift @@ -32,4 +32,12 @@ import Foundation @objc init(outerLabel representableProperty: Int, customProperty: MyOtherClassWrapper) { wrappedInstance = MyClass(outerLabel: representableProperty, customProperty: customProperty.wrappedInstance) } + + @objc init?(outerLabel x: Int) { + if let instance = MyClass(outerLabel: x) { + wrappedInstance = instance + } else { + return nil + } + } } diff --git a/pkgs/swift2objc/test/integration/integration_test.dart b/pkgs/swift2objc/test/integration/integration_test.dart index 5a1317d3d..e762ce9cb 100644 --- a/pkgs/swift2objc/test/integration/integration_test.dart +++ b/pkgs/swift2objc/test/integration/integration_test.dart @@ -4,6 +4,7 @@ import 'dart:io'; +import 'package:logging/logging.dart'; import 'package:path/path.dart' as path; import 'package:swift2objc/swift2objc.dart'; import 'package:test/test.dart'; @@ -11,6 +12,10 @@ import 'package:test/test.dart'; const regenerateExpectedOutputs = false; void main() { + Logger.root.onRecord.listen((record) { + stderr.writeln('${record.level.name}: ${record.message}'); + }); + group('Integration tests', () { const inputSuffix = '_input.swift'; const outputSuffix = '_output.swift'; diff --git a/pkgs/swift2objc/test/integration/structs_and_initializers_input.swift b/pkgs/swift2objc/test/integration/structs_and_initializers_input.swift index f8617f0e2..fa505854d 100644 --- a/pkgs/swift2objc/test/integration/structs_and_initializers_input.swift +++ b/pkgs/swift2objc/test/integration/structs_and_initializers_input.swift @@ -8,6 +8,15 @@ public class MyStruct { self.representableProperty = representableProperty self.customProperty = customProperty } + + public init?(outerLabel x: Int) { + if x == 0 { + return nil + } else { + self.representableProperty = x + self.customProperty = MyOtherStruct() + } + } } public struct MyOtherStruct {} diff --git a/pkgs/swift2objc/test/integration/structs_and_initializers_output.swift b/pkgs/swift2objc/test/integration/structs_and_initializers_output.swift index 72ee5e4ea..14031a883 100644 --- a/pkgs/swift2objc/test/integration/structs_and_initializers_output.swift +++ b/pkgs/swift2objc/test/integration/structs_and_initializers_output.swift @@ -20,4 +20,12 @@ import Foundation @objc init(outerLabel representableProperty: Int, customProperty: MyOtherStructWrapper) { wrappedInstance = MyStruct(outerLabel: representableProperty, customProperty: customProperty.wrappedInstance) } + + @objc init?(outerLabel x: Int) { + if let instance = MyStruct(outerLabel: x) { + wrappedInstance = instance + } else { + return nil + } + } } diff --git a/pkgs/swift2objc/test/unit/parse_initializer_param_test.dart b/pkgs/swift2objc/test/unit/parse_initializer_param_test.dart index 05693aa35..26a7707ec 100644 --- a/pkgs/swift2objc/test/unit/parse_initializer_param_test.dart +++ b/pkgs/swift2objc/test/unit/parse_initializer_param_test.dart @@ -34,30 +34,28 @@ void main() { test('Two params with one internal name', () { final json = Json(jsonDecode( ''' - { - "declarationFragments": [ - { "kind": "keyword", "spelling": "init" }, - { "kind": "text", "spelling": "(" }, - { "kind": "externalParam", "spelling": "outerLabel" }, - { "kind": "text", "spelling": " " }, - { "kind": "internalParam", "spelling": "internalLabel" }, - { "kind": "text", "spelling": ": " }, - { - "kind": "typeIdentifier", - "spelling": "Int", - "preciseIdentifier": "s:Si" - }, - { "kind": "text", "spelling": ", " }, - { "kind": "externalParam", "spelling": "singleLabel" }, - { "kind": "text", "spelling": ": " }, - { - "kind": "typeIdentifier", - "spelling": "Int", - "preciseIdentifier": "s:Si" - }, - { "kind": "text", "spelling": ")" } - ] - } + [ + { "kind": "keyword", "spelling": "init" }, + { "kind": "text", "spelling": "(" }, + { "kind": "externalParam", "spelling": "outerLabel" }, + { "kind": "text", "spelling": " " }, + { "kind": "internalParam", "spelling": "internalLabel" }, + { "kind": "text", "spelling": ": " }, + { + "kind": "typeIdentifier", + "spelling": "Int", + "preciseIdentifier": "s:Si" + }, + { "kind": "text", "spelling": ", " }, + { "kind": "externalParam", "spelling": "singleLabel" }, + { "kind": "text", "spelling": ": " }, + { + "kind": "typeIdentifier", + "spelling": "Int", + "preciseIdentifier": "s:Si" + }, + { "kind": "text", "spelling": ")" } + ] ''', )); @@ -80,20 +78,18 @@ void main() { test('One param', () { final json = Json(jsonDecode( ''' - { - "declarationFragments": [ - { "kind": "keyword", "spelling": "init" }, - { "kind": "text", "spelling": "(" }, - { "kind": "externalParam", "spelling": "parameter" }, - { "kind": "text", "spelling": ": " }, - { - "kind": "typeIdentifier", - "spelling": "Int", - "preciseIdentifier": "s:Si" - }, - { "kind": "text", "spelling": ")" } - ] - } + [ + { "kind": "keyword", "spelling": "init" }, + { "kind": "text", "spelling": "(" }, + { "kind": "externalParam", "spelling": "parameter" }, + { "kind": "text", "spelling": ": " }, + { + "kind": "typeIdentifier", + "spelling": "Int", + "preciseIdentifier": "s:Si" + }, + { "kind": "text", "spelling": ")" } + ] ''', )); @@ -112,12 +108,10 @@ void main() { test('No params', () { final json = Json(jsonDecode( ''' - { - "declarationFragments": [ - { "kind": "keyword", "spelling": "init" }, - { "kind": "text", "spelling": "()" } - ] - } + [ + { "kind": "keyword", "spelling": "init" }, + { "kind": "text", "spelling": "()" } + ] ''', )); @@ -131,20 +125,18 @@ void main() { test('Parameter with no outer label', () { final json = Json(jsonDecode( ''' - { - "declarationFragments": [ - { "kind": "keyword", "spelling": "init" }, - { "kind": "text", "spelling": "(" }, - { "kind": "internalParam", "spelling": "internalLabel" }, - { "kind": "text", "spelling": ": " }, - { - "kind": "typeIdentifier", - "spelling": "Int", - "preciseIdentifier": "s:Si" - }, - { "kind": "text", "spelling": ")" } - ] - } + [ + { "kind": "keyword", "spelling": "init" }, + { "kind": "text", "spelling": "(" }, + { "kind": "internalParam", "spelling": "internalLabel" }, + { "kind": "text", "spelling": ": " }, + { + "kind": "typeIdentifier", + "spelling": "Int", + "preciseIdentifier": "s:Si" + }, + { "kind": "text", "spelling": ")" } + ] ''', )); @@ -157,16 +149,14 @@ void main() { test('Parameter with no type', () { final json = Json(jsonDecode( ''' - { - "declarationFragments": [ - { "kind": "keyword", "spelling": "init" }, - { "kind": "text", "spelling": "(" }, - { "kind": "externalParam", "spelling": "outerLabel" }, - { "kind": "text", "spelling": " " }, - { "kind": "internalParam", "spelling": "internalLabel" }, - { "kind": "text", "spelling": ")" } - ] - } + [ + { "kind": "keyword", "spelling": "init" }, + { "kind": "text", "spelling": "(" }, + { "kind": "externalParam", "spelling": "outerLabel" }, + { "kind": "text", "spelling": " " }, + { "kind": "internalParam", "spelling": "internalLabel" }, + { "kind": "text", "spelling": ")" } + ] ''', )); @@ -179,19 +169,17 @@ void main() { test('Parameter with just a type (no label)', () { final json = Json(jsonDecode( ''' - { - "declarationFragments": [ - { "kind": "keyword", "spelling": "init" }, - { "kind": "text", "spelling": "(" }, - { "kind": "text", "spelling": ": " }, - { - "kind": "typeIdentifier", - "spelling": "Int", - "preciseIdentifier": "s:Si" - }, - { "kind": "text", "spelling": ")" } - ] - } + [ + { "kind": "keyword", "spelling": "init" }, + { "kind": "text", "spelling": "(" }, + { "kind": "text", "spelling": ": " }, + { + "kind": "typeIdentifier", + "spelling": "Int", + "preciseIdentifier": "s:Si" + }, + { "kind": "text", "spelling": ")" } + ] ''', ));