Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[swift2objc] Support failable initializers #1734

Merged
merged 2 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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