Skip to content

Commit

Permalink
[swift2objc] Support failable initializers (#1734)
Browse files Browse the repository at this point in the history
  • Loading branch information
liamappelbe authored Nov 19, 2024
1 parent 8165cbb commit b2aca7e
Show file tree
Hide file tree
Showing 15 changed files with 178 additions and 109 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ class InitializerDeclaration
@override
bool isOverriding;

bool isFailable;

@override
List<Parameter> params;

Expand All @@ -41,5 +43,6 @@ class InitializerDeclaration
this.statements = const [],
required this.hasObjCAnnotation,
required this.isOverriding,
required this.isFailable,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,13 @@ List<String> _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');
Expand Down
4 changes: 4 additions & 0 deletions pkgs/swift2objc/lib/src/parser/_core/json.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -101,6 +102,9 @@ class Json extends IterableBase<Json> {
),
);
}

@override
String toString() => jsonEncode(_json);
}

class _JsonIterator implements Iterator<Json> {
Expand Down
23 changes: 9 additions & 14 deletions pkgs/swift2objc/lib/src/parser/_core/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ extension TopLevelOnly<T extends Declaration> on List<T> {
).toList();
}

/// Matches fragments, which look like {"kind": "foo", "spelling": "bar"}.
bool matchFragment(Json fragment, String kind, String spelling) =>
fragment['kind'].get<String?>() == kind &&
fragment['spelling'].get<String?>() == spelling;

String parseSymbolId(Json symbolJson) {
final idJson = symbolJson['identifier']['precise'];
final id = idJson.get<String>();
Expand All @@ -56,23 +61,13 @@ String parseSymbolName(Json symbolJson) {
}

bool parseSymbolHasObjcAnnotation(Json symbolJson) {
return symbolJson['declarationFragments'].any(
(json) =>
json['kind'].exists &&
json['kind'].get<String>() == 'attribute' &&
json['spelling'].exists &&
json['spelling'].get<String>() == '@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<String>() == 'keyword' &&
json['spelling'].exists &&
json['spelling'].get<String>() == 'override',
);
return symbolJson['declarationFragments']
.any((json) => matchFragment(json, 'keyword', 'override'));
}

ReferredType parseTypeFromId(String typeId, ParsedSymbolgraph symbolgraph) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Parameter> 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:
Expand Down Expand Up @@ -51,7 +62,7 @@ List<Parameter> parseInitializerParams(

final parameters = <Parameter>[];

for (final fragmentJson in fragments) {
for (final fragmentJson in declarationFragments) {
final kind = fragmentJson['kind'].get<String>();
final invalidOrderException = Exception(
'Invalid fragments order at ${fragmentJson.path}',
Expand Down Expand Up @@ -94,7 +105,7 @@ List<Parameter> 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}',
);
}

Expand Down
7 changes: 6 additions & 1 deletion pkgs/swift2objc/lib/src/transformer/_core/unique_namer.dart
Original file line number Diff line number Diff line change
@@ -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<String> _usedNames;

UniqueNamer(Iterable<String> usedNames) : _usedNames = usedNames.toSet();
UniqueNamer([Iterable<String> usedNames = const <String>[]])
: _usedNames = usedNames.toSet();

UniqueNamer.inCompound(CompoundDeclaration compoundDeclaration)
: _usedNames = {
Expand Down
4 changes: 4 additions & 0 deletions pkgs/swift2objc/lib/src/transformer/_core/utils.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ InitializerDeclaration _buildWrapperInitializer(
)
],
isOverriding: false,
isFailable: false,
statements: ['self.${wrappedClassInstance.name} = wrappedInstance'],
hasObjCAnnotation: wrappedClassInstance.hasObjCAnnotation,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -54,13 +55,14 @@ List<String> _generateInitializerStatements(
InitializerDeclaration transformedInitializer,
) {
final argumentsList = <String>[];
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,
Expand All @@ -77,5 +79,16 @@ List<String> _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'];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
5 changes: 5 additions & 0 deletions pkgs/swift2objc/test/integration/integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@

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';

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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
Loading

0 comments on commit b2aca7e

Please sign in to comment.