diff --git a/pkgs/swift2objc/lib/src/ast/_core/interfaces/can_throw.dart b/pkgs/swift2objc/lib/src/ast/_core/interfaces/can_throw.dart new file mode 100644 index 000000000..a7ef31385 --- /dev/null +++ b/pkgs/swift2objc/lib/src/ast/_core/interfaces/can_throw.dart @@ -0,0 +1,9 @@ +// 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. + +/// An interface to describe a Swift entity's ability to be annotated +/// with `throws`. +abstract interface class CanThrow { + abstract final bool throws; +} diff --git a/pkgs/swift2objc/lib/src/ast/_core/interfaces/function_declaration.dart b/pkgs/swift2objc/lib/src/ast/_core/interfaces/function_declaration.dart index a354941a8..ca90dc5c7 100644 --- a/pkgs/swift2objc/lib/src/ast/_core/interfaces/function_declaration.dart +++ b/pkgs/swift2objc/lib/src/ast/_core/interfaces/function_declaration.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import '../shared/referred_type.dart'; +import 'can_throw.dart'; import 'declaration.dart'; import 'executable.dart'; import 'parameterizable.dart'; @@ -10,6 +11,11 @@ import 'type_parameterizable.dart'; /// Describes a function-like entity. abstract interface class FunctionDeclaration - implements Declaration, Parameterizable, Executable, TypeParameterizable { + implements + Declaration, + Parameterizable, + Executable, + TypeParameterizable, + CanThrow { abstract final ReferredType returnType; } diff --git a/pkgs/swift2objc/lib/src/ast/_core/interfaces/overridable.dart b/pkgs/swift2objc/lib/src/ast/_core/interfaces/overridable.dart index 937991701..2ec2d385a 100644 --- a/pkgs/swift2objc/lib/src/ast/_core/interfaces/overridable.dart +++ b/pkgs/swift2objc/lib/src/ast/_core/interfaces/overridable.dart @@ -3,7 +3,7 @@ // BSD-style license that can be found in the LICENSE file. /// An interface to describe a Swift entity's ability to be annotated -/// with `@objc`. +/// with `override`. abstract interface class Overridable { abstract final bool isOverriding; } diff --git a/pkgs/swift2objc/lib/src/ast/_core/interfaces/variable_declaration.dart b/pkgs/swift2objc/lib/src/ast/_core/interfaces/variable_declaration.dart index 1f024735b..c2c694e35 100644 --- a/pkgs/swift2objc/lib/src/ast/_core/interfaces/variable_declaration.dart +++ b/pkgs/swift2objc/lib/src/ast/_core/interfaces/variable_declaration.dart @@ -3,10 +3,15 @@ // BSD-style license that can be found in the LICENSE file. import '../shared/referred_type.dart'; +import 'can_throw.dart'; import 'declaration.dart'; /// Describes a variable-like entity. -abstract interface class VariableDeclaration implements Declaration { +/// +/// This declaration [CanThrow] because Swift variables can have explicit +/// getters, which can be marked with `throws`. Such variables may not have a +/// setter. +abstract interface class VariableDeclaration implements Declaration, CanThrow { abstract final bool isConstant; abstract final ReferredType type; } 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 12d19698e..1fdf10d96 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 @@ -2,6 +2,7 @@ // 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 '../../../_core/interfaces/can_throw.dart'; import '../../../_core/interfaces/declaration.dart'; import '../../../_core/interfaces/executable.dart'; import '../../../_core/interfaces/objc_annotatable.dart'; @@ -16,7 +17,8 @@ class InitializerDeclaration Executable, Parameterizable, ObjCAnnotatable, - Overridable { + Overridable, + CanThrow { @override String id; @@ -29,6 +31,9 @@ class InitializerDeclaration @override bool isOverriding; + @override + bool throws; + bool isFailable; @override @@ -48,6 +53,7 @@ class InitializerDeclaration this.statements = const [], required this.hasObjCAnnotation, required this.isOverriding, + required this.throws, required this.isFailable, }); } diff --git a/pkgs/swift2objc/lib/src/ast/declarations/compounds/members/method_declaration.dart b/pkgs/swift2objc/lib/src/ast/declarations/compounds/members/method_declaration.dart index 849dac94e..72315a962 100644 --- a/pkgs/swift2objc/lib/src/ast/declarations/compounds/members/method_declaration.dart +++ b/pkgs/swift2objc/lib/src/ast/declarations/compounds/members/method_declaration.dart @@ -30,6 +30,9 @@ class MethodDeclaration @override bool isOverriding; + @override + bool throws; + @override List statements; @@ -53,5 +56,6 @@ class MethodDeclaration this.statements = const [], this.isStatic = false, this.isOverriding = false, + this.throws = false, }) : assert(!isStatic || !isOverriding); } diff --git a/pkgs/swift2objc/lib/src/ast/declarations/compounds/members/property_declaration.dart b/pkgs/swift2objc/lib/src/ast/declarations/compounds/members/property_declaration.dart index 536edf414..adf151e51 100644 --- a/pkgs/swift2objc/lib/src/ast/declarations/compounds/members/property_declaration.dart +++ b/pkgs/swift2objc/lib/src/ast/declarations/compounds/members/property_declaration.dart @@ -25,6 +25,9 @@ class PropertyDeclaration implements VariableDeclaration, ObjCAnnotatable { @override bool isConstant; + @override + bool throws; + bool hasSetter; PropertyStatements? getter; @@ -42,7 +45,9 @@ class PropertyDeclaration implements VariableDeclaration, ObjCAnnotatable { this.getter, this.setter, this.isStatic = false, - }) : assert(!isConstant || !hasSetter); + this.throws = false, + }) : assert(!(isConstant && hasSetter)), + assert(!(hasSetter && throws)); } class PropertyStatements implements Executable { diff --git a/pkgs/swift2objc/lib/src/ast/declarations/globals/globals.dart b/pkgs/swift2objc/lib/src/ast/declarations/globals/globals.dart index 73c55ffa1..1dd6c248b 100644 --- a/pkgs/swift2objc/lib/src/ast/declarations/globals/globals.dart +++ b/pkgs/swift2objc/lib/src/ast/declarations/globals/globals.dart @@ -33,6 +33,9 @@ class GlobalFunctionDeclaration implements FunctionDeclaration { @override List typeParams; + @override + bool throws; + @override ReferredType returnType; @@ -46,6 +49,7 @@ class GlobalFunctionDeclaration implements FunctionDeclaration { required this.returnType, this.typeParams = const [], this.statements = const [], + this.throws = false, }); } @@ -63,10 +67,14 @@ class GlobalVariableDeclaration implements VariableDeclaration { @override bool isConstant; + @override + bool throws; + GlobalVariableDeclaration({ required this.id, required this.name, required this.type, required this.isConstant, - }); + required this.throws, + }) : assert(!(throws && !isConstant)); } diff --git a/pkgs/swift2objc/lib/src/generator/generators/class_generator.dart b/pkgs/swift2objc/lib/src/generator/generators/class_generator.dart index 2b4c32efb..c54837fd7 100644 --- a/pkgs/swift2objc/lib/src/generator/generators/class_generator.dart +++ b/pkgs/swift2objc/lib/src/generator/generators/class_generator.dart @@ -87,6 +87,10 @@ List _generateInitializer(InitializerDeclaration initializer) { header.write('(${generateParameters(initializer.params)})'); + if (initializer.throws) { + header.write(' throws'); + } + return [ '$header {', ...initializer.statements.indent(), @@ -116,6 +120,10 @@ List _generateClassMethod(MethodDeclaration method) { 'public func ${method.name}(${generateParameters(method.params)})', ); + if (method.throws) { + header.write(' throws'); + } + if (!method.returnType.sameAs(voidType)) { header.write(' -> ${method.returnType.swiftType}'); } diff --git a/pkgs/swift2objc/lib/src/parser/_core/json.dart b/pkgs/swift2objc/lib/src/parser/_core/json.dart index 3f8fd5a59..aa0d1a0f5 100644 --- a/pkgs/swift2objc/lib/src/parser/_core/json.dart +++ b/pkgs/swift2objc/lib/src/parser/_core/json.dart @@ -2,7 +2,6 @@ // 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 'dart:collection'; import 'dart:convert'; /// This is a helper class that helps with parsing Json values. It supports @@ -18,13 +17,13 @@ import 'dart:convert'; /// The class is also an `Iterable` so if the json is an array, you can directly /// iterate over it with a `for` loop. If the json isn't an array, attempting to /// iterate over it will throw an error. -class Json extends IterableBase { - final List _pathSegments; +class Json extends Iterable { + final List pathSegments; final dynamic _json; - String get path => _pathSegments.join('/'); + String get path => pathSegments.join('/'); - Json(this._json, [this._pathSegments = const []]); + Json(this._json, [this.pathSegments = const []]); /// The subscript syntax is intended to access a value at a field of a map or /// at an index if an array, and thus, the `index` parameter here can either @@ -37,7 +36,7 @@ class Json extends IterableBase { 'Expected a map at "$path", found a ${_json.runtimeType}', ); } - return Json(_json[index], [..._pathSegments, index]); + return Json(_json[index], [...pathSegments, index]); } if (index is int) { @@ -57,7 +56,7 @@ class Json extends IterableBase { 'Invalid negative index at "$path" (supplied index: $index)', ); } - return Json(_json[index], [..._pathSegments, '$index']); + return Json(_json[index], [...pathSegments, '$index']); } throw Exception( @@ -115,7 +114,7 @@ class _JsonIterator implements Iterator { _JsonIterator(this._json) : _list = _json.get(); @override - Json get current => Json(_list[_index], [..._json._pathSegments, '$_index']); + Json get current => Json(_list[_index], [..._json.pathSegments, '$_index']); @override bool moveNext() { diff --git a/pkgs/swift2objc/lib/src/parser/_core/token_list.dart b/pkgs/swift2objc/lib/src/parser/_core/token_list.dart index 8939d1036..46b7d3c44 100644 --- a/pkgs/swift2objc/lib/src/parser/_core/token_list.dart +++ b/pkgs/swift2objc/lib/src/parser/_core/token_list.dart @@ -2,6 +2,8 @@ // 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 'package:meta/meta.dart'; + import 'json.dart'; import 'utils.dart'; @@ -15,7 +17,7 @@ import 'utils.dart'; /// can pass it to parseType, because certain tokens get concatenated by the /// swift compiler. This class performs that preprocessing, as well as providing /// convenience methods for parsing, like slicing. -class TokenList { +class TokenList extends Iterable { final List _list; final int _start; final int _end; @@ -24,30 +26,55 @@ class TokenList { : assert(0 <= _start && _start <= _end && _end <= _list.length); factory TokenList(Json fragments) { - const splits = { - '?(': ['?', '('], - '?)': ['?', ')'], - '?, ': ['?', ', '], - ') -> ': [')', '->'], - '?) -> ': ['?', ')', '->'], - }; - - final list = []; - for (final token in fragments) { - final split = splits[getSpellingForKind(token, 'text')]; - if (split != null) { - for (final sub in split) { - list.add(Json({'kind': 'text', 'spelling': sub})); + final list = [for (final token in fragments) ...splitToken(token)]; + return TokenList._(list, 0, list.length); + } + + @visibleForTesting + static Iterable splitToken(Json token) sync* { + 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. + 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. + yield textToken(text); + return; + } + + var suffix = 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; + break; } - } else { - list.add(token); + } + if (!any) { + // Remaining text isn't splittable. + if (suffix.isNotEmpty) yield textToken(suffix); + break; } } - return TokenList._(list, 0, list.length); } + @override int get length => _end - _start; - bool get isEmpty => length == 0; + + @override + Iterator get iterator => _TokenListIterator(this); + Json operator [](int index) => _list[index + _start]; int indexWhere(bool Function(Json element) test) { @@ -63,3 +90,19 @@ class TokenList { @override String toString() => _list.getRange(_start, _end).toString(); } + +class _TokenListIterator implements Iterator { + final TokenList _list; + var _index = -1; + + _TokenListIterator(this._list); + + @override + Json get current => _list[_index]; + + @override + bool moveNext() { + _index++; + return _index < _list.length; + } +} diff --git a/pkgs/swift2objc/lib/src/parser/_core/utils.dart b/pkgs/swift2objc/lib/src/parser/_core/utils.dart index 398986b29..7c42046f0 100644 --- a/pkgs/swift2objc/lib/src/parser/_core/utils.dart +++ b/pkgs/swift2objc/lib/src/parser/_core/utils.dart @@ -107,7 +107,7 @@ ReferredType parseTypeAfterSeparator( ) { // fragments = [..., ': ', type tokens...] final separatorIndex = - fragments.indexWhere((token) => matchFragment(token, 'text', ': ')); + fragments.indexWhere((token) => matchFragment(token, 'text', ':')); final (type, suffix) = parseType(symbolgraph, fragments.slice(separatorIndex + 1)); assert(suffix.isEmpty, '$suffix'); 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 27a4e35b5..6f0acf973 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 @@ -16,12 +16,14 @@ GlobalFunctionDeclaration parseGlobalFunctionDeclaration( Json globalFunctionSymbolJson, ParsedSymbolgraph symbolgraph, ) { + final info = parseFunctionInfo( + globalFunctionSymbolJson['declarationFragments'], symbolgraph); return GlobalFunctionDeclaration( id: parseSymbolId(globalFunctionSymbolJson), name: parseSymbolName(globalFunctionSymbolJson), returnType: _parseFunctionReturnType(globalFunctionSymbolJson, symbolgraph), - params: parseFunctionParams( - globalFunctionSymbolJson['declarationFragments'], symbolgraph), + params: info.params, + throws: info.throws, ); } @@ -30,29 +32,38 @@ MethodDeclaration parseMethodDeclaration( ParsedSymbolgraph symbolgraph, { bool isStatic = false, }) { + final info = + parseFunctionInfo(methodSymbolJson['declarationFragments'], symbolgraph); return MethodDeclaration( id: parseSymbolId(methodSymbolJson), name: parseSymbolName(methodSymbolJson), returnType: _parseFunctionReturnType(methodSymbolJson, symbolgraph), - params: parseFunctionParams( - methodSymbolJson['declarationFragments'], symbolgraph), + params: info.params, hasObjCAnnotation: parseSymbolHasObjcAnnotation(methodSymbolJson), isStatic: isStatic, + throws: info.throws, ); } -List parseFunctionParams( +typedef ParsedFunctionInfo = ({ + List params, + bool throws, +}); + +ParsedFunctionInfo parseFunctionInfo( Json declarationFragments, ParsedSymbolgraph symbolgraph, ) { - // `declarationFragments` describes each part of the initializer declaration, + // `declarationFragments` describes each part of the function declaration, // things like the `func` keyword, brackets, spaces, etc. We only care about - // the parameter fragments here, and they always appear in this order: + // the parameter fragments and annotations here, and they always appear in + // this order: // [ // ..., '(', // externalParam, ' ', internalParam, ': ', type..., ', ' // externalParam, ': ', type..., ', ' // externalParam, ' ', internalParam, ': ', type..., ')' + // annotations..., '->', returnType... // ] // Note: `internalParam` may or may not exist. // @@ -60,34 +71,40 @@ List parseFunctionParams( // while making sure the parameter fragments have the expected order. final parameters = []; + final malformedInitializerException = Exception( + 'Malformed parameter list at ${declarationFragments.path}: ' + '$declarationFragments', + ); var tokens = TokenList(declarationFragments); + String? maybeConsume(String kind) { + if (tokens.isEmpty) return null; + final spelling = getSpellingForKind(tokens[0], kind); + if (spelling != null) tokens = tokens.slice(1); + return spelling; + } + final openParen = tokens.indexWhere((tok) => matchFragment(tok, 'text', '(')); - if (openParen != -1) { - tokens = tokens.slice(openParen + 1); - String? consume(String kind) { - if (tokens.isEmpty) return null; - final token = tokens[0]; - tokens = tokens.slice(1); - return getSpellingForKind(token, kind); - } + if (openParen == -1) throw malformedInitializerException; + tokens = tokens.slice(openParen + 1); - final malformedInitializerException = Exception( - 'Malformed initializer at ${declarationFragments.path}', - ); + // Parse parameters until we find a ')'. + if (maybeConsume('text') == ')') { + // Empty param list. + } else { while (true) { - final externalParam = consume('externalParam'); + final externalParam = maybeConsume('externalParam'); if (externalParam == null) throw malformedInitializerException; - var sep = consume('text'); + var sep = maybeConsume('text'); String? internalParam; - if (sep == ' ') { - internalParam = consume('internalParam'); + if (sep == '') { + internalParam = maybeConsume('internalParam'); if (internalParam == null) throw malformedInitializerException; - sep = consume('text'); + sep = maybeConsume('text'); } - if (sep != ': ') throw malformedInitializerException; + if (sep != ':') throw malformedInitializerException; final (type, remainingTokens) = parseType(symbolgraph, tokens); tokens = remainingTokens; @@ -97,16 +114,24 @@ List parseFunctionParams( type: type, )); - final end = consume('text'); + final end = maybeConsume('text'); if (end == ')') break; - if (end != ', ') throw malformedInitializerException; - } - if (!(tokens.isEmpty || consume('text') == '->')) { - throw malformedInitializerException; + if (end != ',') throw malformedInitializerException; } } - return parameters; + // Parse annotations until we run out. + final annotations = {}; + while (true) { + final keyword = maybeConsume('keyword'); + if (keyword == null) break; + annotations.add(keyword); + } + + return ( + params: parameters, + throws: annotations.contains('throws'), + ); } ReferredType _parseFunctionReturnType( 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 6d5b725a7..b9ce98411 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 @@ -19,12 +19,14 @@ InitializerDeclaration parseInitializerDeclaration( throw Exception('Invalid initializer at ${declarationFragments.path}: $id'); } + final info = parseFunctionInfo(declarationFragments, symbolgraph); return InitializerDeclaration( id: id, - params: parseFunctionParams(declarationFragments, symbolgraph), + params: info.params, hasObjCAnnotation: parseSymbolHasObjcAnnotation(initializerSymbolJson), isOverriding: parseIsOverriding(initializerSymbolJson), isFailable: parseIsFailableInit(id, declarationFragments), + throws: info.throws, ); } diff --git a/pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_variable_declaration.dart b/pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_variable_declaration.dart index a2288404e..b1aa8c78d 100644 --- a/pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_variable_declaration.dart +++ b/pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_variable_declaration.dart @@ -24,6 +24,7 @@ PropertyDeclaration parsePropertyDeclaration( isConstant: isConstant, hasSetter: isConstant ? false : _parsePropertyHasSetter(propertySymbolJson), isStatic: isStatic, + throws: _parseVariableThrows(propertySymbolJson), ); } @@ -39,6 +40,7 @@ GlobalVariableDeclaration parseGlobalVariableDeclaration( name: parseSymbolName(variableSymbolJson), type: _parseVariableType(variableSymbolJson, symbolgraph), isConstant: isConstant || !hasSetter, + throws: _parseVariableThrows(variableSymbolJson), ); } @@ -52,36 +54,37 @@ ReferredType _parseVariableType( bool _parseVariableIsConstant(Json variableSymbolJson) { final fragmentsJson = variableSymbolJson['declarationFragments']; - final declarationKeywordJson = fragmentsJson.firstWhere( - (json) { - if (json['kind'].get() != 'keyword') return false; - - final keyword = json['spelling'].get(); - if (keyword != 'var' && keyword != 'let') return false; - - return true; - }, + final declarationKeyword = fragmentsJson.firstWhere( + (json) => + matchFragment(json, 'keyword', 'var') || + matchFragment(json, 'keyword', 'let'), orElse: () => throw ArgumentError( 'Invalid property declaration fragments at path: ${fragmentsJson.path}. ' 'Expected to find "var" or "let" as a keyword, found none', ), ); - final declarationKeyword = declarationKeywordJson['spelling'].get(); + return matchFragment(declarationKeyword, 'keyword', 'let'); +} - return declarationKeyword == 'let'; +bool _parseVariableThrows(Json json) { + final throws = json['declarationFragments'] + .any((frag) => matchFragment(frag, 'keyword', 'throws')); + if (throws) { + // TODO(https://github.com/dart-lang/native/issues/1765): Support throwing + // getters. + throw Exception("Throwing getters aren't supported yet, at ${json.path}"); + } + return throws; } bool _parsePropertyHasSetter(Json propertySymbolJson) { final fragmentsJson = propertySymbolJson['declarationFragments']; - final hasExplicitSetter = fragmentsJson.any( - (json) => json['spelling'].get() == 'set', - ); - - final hasExplicitGetter = fragmentsJson.any( - (json) => json['spelling'].get() == 'get', - ); + final hasExplicitSetter = + fragmentsJson.any((frag) => matchFragment(frag, 'keyword', 'set')); + final hasExplicitGetter = + fragmentsJson.any((frag) => matchFragment(frag, 'keyword', 'get')); if (hasExplicitGetter) { if (hasExplicitSetter) { diff --git a/pkgs/swift2objc/lib/src/parser/parsers/parse_type.dart b/pkgs/swift2objc/lib/src/parser/parsers/parse_type.dart index a6e97a5d6..6e7a4231f 100644 --- a/pkgs/swift2objc/lib/src/parser/parsers/parse_type.dart +++ b/pkgs/swift2objc/lib/src/parser/parsers/parse_type.dart @@ -80,13 +80,18 @@ typedef PrefixParselet = (ReferredType, TokenList) Function( return (type, fragments); } -(ReferredType, TokenList) _emptyTupleParselet( - ParsedSymbolgraph symbolgraph, Json token, TokenList fragments) => - (voidType, fragments); +(ReferredType, TokenList) _tupleParselet( + ParsedSymbolgraph symbolgraph, Json token, TokenList fragments) { + final nextToken = fragments[0]; + if (_tokenId(nextToken) != 'text: )') { + throw Exception('Tuples not supported yet, at ${token.path}'); + } + return (voidType, fragments.slice(1)); +} Map _prefixParsets = { 'typeIdentifier': _typeIdentifierParselet, - 'text: ()': _emptyTupleParselet, + 'text: (': _tupleParselet, }; // ======================== diff --git a/pkgs/swift2objc/lib/src/transformer/transformers/transform_compound.dart b/pkgs/swift2objc/lib/src/transformer/transformers/transform_compound.dart index 4b5047820..d01bd06f8 100644 --- a/pkgs/swift2objc/lib/src/transformer/transformers/transform_compound.dart +++ b/pkgs/swift2objc/lib/src/transformer/transformers/transform_compound.dart @@ -100,6 +100,7 @@ InitializerDeclaration _buildWrapperInitializer( ], isOverriding: false, isFailable: false, + throws: false, statements: ['self.${wrappedClassInstance.name} = wrappedInstance'], hasObjCAnnotation: wrappedClassInstance.hasObjCAnnotation, ); diff --git a/pkgs/swift2objc/lib/src/transformer/transformers/transform_function.dart b/pkgs/swift2objc/lib/src/transformer/transformers/transform_function.dart index eebfbbc69..a1ce97dc0 100644 --- a/pkgs/swift2objc/lib/src/transformer/transformers/transform_function.dart +++ b/pkgs/swift2objc/lib/src/transformer/transformers/transform_function.dart @@ -99,6 +99,7 @@ MethodDeclaration _transformFunction( isStatic: originalFunction is MethodDeclaration ? originalFunction.isStatic : true, + throws: originalFunction.throws, ); transformedMethod.statements = _generateStatements( @@ -148,7 +149,10 @@ List _generateStatements( final localNamer = UniqueNamer(); final arguments = generateInvocationParams( localNamer, originalFunction.params, transformedMethod.params); - final originalMethodCall = originalCallGenerator(arguments); + var originalMethodCall = originalCallGenerator(arguments); + if (transformedMethod.throws) { + originalMethodCall = 'try $originalMethodCall'; + } if (originalFunction.returnType.sameAs(transformedMethod.returnType)) { return ['return $originalMethodCall']; diff --git a/pkgs/swift2objc/lib/src/transformer/transformers/transform_initializer.dart b/pkgs/swift2objc/lib/src/transformer/transformers/transform_initializer.dart index 08a065bbd..a7af3c365 100644 --- a/pkgs/swift2objc/lib/src/transformer/transformers/transform_initializer.dart +++ b/pkgs/swift2objc/lib/src/transformer/transformers/transform_initializer.dart @@ -35,6 +35,7 @@ InitializerDeclaration transformInitializer( params: transformedParams, hasObjCAnnotation: true, isFailable: originalInitializer.isFailable, + throws: originalInitializer.throws, // 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. @@ -57,8 +58,11 @@ List _generateInitializerStatements( final localNamer = UniqueNamer(); final arguments = generateInvocationParams( localNamer, originalInitializer.params, transformedInitializer.params); - final instanceConstruction = + var instanceConstruction = '${wrappedClassInstance.type.swiftType}($arguments)'; + if (transformedInitializer.throws) { + instanceConstruction = 'try $instanceConstruction'; + } if (originalInitializer.isFailable) { final instance = localNamer.makeUnique('instance'); return [ diff --git a/pkgs/swift2objc/lib/src/transformer/transformers/transform_variable.dart b/pkgs/swift2objc/lib/src/transformer/transformers/transform_variable.dart index c6da95e8c..9c061542e 100644 --- a/pkgs/swift2objc/lib/src/transformer/transformers/transform_variable.dart +++ b/pkgs/swift2objc/lib/src/transformer/transformers/transform_variable.dart @@ -81,6 +81,7 @@ PropertyDeclaration _transformVariable( ? originalVariable.isStatic : true, isConstant: originalVariable.isConstant, + throws: originalVariable.throws, ); final getterStatements = _generateGetterStatements( diff --git a/pkgs/swift2objc/pubspec.yaml b/pkgs/swift2objc/pubspec.yaml index a0b28fcef..f49adf983 100644 --- a/pkgs/swift2objc/pubspec.yaml +++ b/pkgs/swift2objc/pubspec.yaml @@ -16,6 +16,7 @@ topics: dependencies: logging: ^1.3.0 + meta: ^1.16.0 path: ^1.9.0 environment: diff --git a/pkgs/swift2objc/test/integration/integration_test.dart b/pkgs/swift2objc/test/integration/integration_test.dart index 81b5400b2..b32e01ad6 100644 --- a/pkgs/swift2objc/test/integration/integration_test.dart +++ b/pkgs/swift2objc/test/integration/integration_test.dart @@ -43,13 +43,16 @@ void main([List? args]) { } } + var loggedErrors = 0; Logger.root.onRecord.listen((record) { stderr.writeln('${record.level.name}: ${record.message}'); + if (record.level >= Level.WARNING) ++loggedErrors; }); group('Integration tests', () { for (final name in testNames) { test(name, () async { + loggedErrors = 0; final inputFile = path.join(thisDir, '$name$inputSuffix'); final expectedOutputFile = path.join(thisDir, '$name$outputSuffix'); final actualOutputFile = regen @@ -69,6 +72,7 @@ void main([List? args]) { final expectedOutput = File(expectedOutputFile).readAsStringSync(); expect(actualOutput, expectedOutput); + expect(loggedErrors, 0); // Try generating symbolgraph for input & output files // to make sure the result compiles. Input file must be included cause diff --git a/pkgs/swift2objc/test/integration/throws_input.swift b/pkgs/swift2objc/test/integration/throws_input.swift new file mode 100644 index 000000000..53186dd80 --- /dev/null +++ b/pkgs/swift2objc/test/integration/throws_input.swift @@ -0,0 +1,11 @@ +import Foundation + +public class MyClass { + public init(y: Int) throws {} + + public func voidMethod() throws {} + public func intMethod(y: Int) throws -> MyClass { return try MyClass(y: 123) } +} + +public func voidFunc(x: Int, y: Int) throws {} +public func intFunc() throws -> MyClass { return try MyClass(y: 123) } diff --git a/pkgs/swift2objc/test/integration/throws_output.swift b/pkgs/swift2objc/test/integration/throws_output.swift new file mode 100644 index 000000000..dd1d16df2 --- /dev/null +++ b/pkgs/swift2objc/test/integration/throws_output.swift @@ -0,0 +1,38 @@ +// Test preamble text + +import Foundation + +@objc public class GlobalsWrapper: NSObject { + @objc static public func intFuncWrapper() throws -> MyClassWrapper { + let result = try intFunc() + return MyClassWrapper(result) + } + + @objc static public func voidFuncWrapper(x: Int, y: Int) throws { + return try voidFunc(x: x, y: y) + } + +} + +@objc public class MyClassWrapper: NSObject { + var wrappedInstance: MyClass + + init(_ wrappedInstance: MyClass) { + self.wrappedInstance = wrappedInstance + } + + @objc init(y: Int) throws { + wrappedInstance = try MyClass(y: y) + } + + @objc public func voidMethod() throws { + return try wrappedInstance.voidMethod() + } + + @objc public func intMethod(y: Int) throws -> MyClassWrapper { + let result = try wrappedInstance.intMethod(y: y) + return MyClassWrapper(result) + } + +} + diff --git a/pkgs/swift2objc/test/unit/parse_function_param_test.dart b/pkgs/swift2objc/test/unit/parse_function_info_test.dart similarity index 90% rename from pkgs/swift2objc/test/unit/parse_function_param_test.dart rename to pkgs/swift2objc/test/unit/parse_function_info_test.dart index e6ca95fed..512a141d1 100644 --- a/pkgs/swift2objc/test/unit/parse_function_param_test.dart +++ b/pkgs/swift2objc/test/unit/parse_function_info_test.dart @@ -63,7 +63,8 @@ void main() { ''', )); - final outputParams = parseFunctionParams(json, emptySymbolgraph); + final info = parseFunctionInfo(json, emptySymbolgraph); + final outputParams = info.params; final expectedParams = [ Parameter( @@ -78,6 +79,7 @@ void main() { ]; expectEqualParams(outputParams, expectedParams); + expect(info.throws, isFalse); }); test('Three params with some optional', () { @@ -118,7 +120,8 @@ void main() { ''', )); - final outputParams = parseFunctionParams(json, emptySymbolgraph); + final info = parseFunctionInfo(json, emptySymbolgraph); + final outputParams = info.params; final expectedParams = [ Parameter( @@ -138,6 +141,7 @@ void main() { ]; expectEqualParams(outputParams, expectedParams); + expect(info.throws, isFalse); }); test('One param', () { @@ -158,7 +162,8 @@ void main() { ''', )); - final outputParams = parseFunctionParams(json, emptySymbolgraph); + final info = parseFunctionInfo(json, emptySymbolgraph); + final outputParams = info.params; final expectedParams = [ Parameter( @@ -168,6 +173,7 @@ void main() { ]; expectEqualParams(outputParams, expectedParams); + expect(info.throws, isFalse); }); test('No params', () { @@ -180,9 +186,11 @@ void main() { ''', )); - final outputParams = parseFunctionParams(json, emptySymbolgraph); + final info = parseFunctionInfo(json, emptySymbolgraph); + final outputParams = info.params; expectEqualParams(outputParams, []); + expect(info.throws, isFalse); }); }); @@ -206,7 +214,7 @@ void main() { )); expect( - () => parseFunctionParams(json, emptySymbolgraph), + () => parseFunctionInfo(json, emptySymbolgraph), throwsA(isA()), ); }); @@ -226,7 +234,7 @@ void main() { )); expect( - () => parseFunctionParams(json, emptySymbolgraph), + () => parseFunctionInfo(json, emptySymbolgraph), throwsA(isA()), ); }); @@ -249,7 +257,7 @@ void main() { )); expect( - () => parseFunctionParams(json, emptySymbolgraph), + () => parseFunctionInfo(json, emptySymbolgraph), throwsA(isA()), ); }); diff --git a/pkgs/swift2objc/test/unit/token_list_test.dart b/pkgs/swift2objc/test/unit/token_list_test.dart index d67b2904b..e3b115f40 100644 --- a/pkgs/swift2objc/test/unit/token_list_test.dart +++ b/pkgs/swift2objc/test/unit/token_list_test.dart @@ -9,9 +9,8 @@ import 'package:swift2objc/src/parser/_core/token_list.dart'; import 'package:test/test.dart'; void main() { - String spelling(TokenList list) => [ - for (var i = 0; i < list.length; ++i) list[i]['spelling'].toString() - ].toString(); + String spelling(Iterable tokens) => + [...tokens.map((t) => t['spelling'])].toString(); test('Slicing', () { final list = TokenList(Json(jsonDecode(''' @@ -51,7 +50,33 @@ void main() { 1); }); - test('Splitting', () { + test('Split one token', () { + List split(String json) => + TokenList.splitToken(Json(jsonDecode(json))).toList(); + expect(spelling(split('{ "kind": "text", "spelling": "a" }')), '["a"]'); + expect(spelling(split('{ "kind": "text", "spelling": "" }')), '[""]'); + expect(spelling(split('{ "kind": "text", "spelling": " " }')), '[""]'); + expect(spelling(split('{ "kind": "text", "spelling": "?" }')), '["?"]'); + expect(spelling(split('{ "kind": "text", "spelling": "???" }')), + '["?", "?", "?"]'); + expect( + spelling(split('{ "kind": "text", "spelling": "()" }')), '["(", ")"]'); + expect(spelling(split('{ "kind": "text", "spelling": " ?) -> () " }')), + '["?", ")", "->", "(", ")"]'); + expect(spelling(split('{ "kind": "typeIdentifier", "spelling": "?)" }')), + '["?)"]'); + + // splitToken gives up as soon as it finds a non-matching prefix. Ideally + // we'd keep splitting out any other tokens we find in the text, but that's + // more complicated to implement (we're writing a full tokenizer at that + // 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??"]'); + }); + + test('Split list', () { final list = TokenList(Json(jsonDecode(''' [ { "kind": "text", "spelling": "a" }, @@ -67,7 +92,7 @@ void main() { '''))); expect(spelling(list), - '["a", "?", "(", "b", "c", "?", ")", "?", ", ", "d", "?(", "e"]'); + '["a", "?", "(", "b", "c", "?", ")", "?", ",", "d", "?(", "e"]'); // If kind != "text", the token isn't changed. expect(list[10].toString(), '{"kind":"typeIdentifier","spelling":"?("}'); diff --git a/pkgs/swiftgen/pubspec.yaml b/pkgs/swiftgen/pubspec.yaml index 664fd220c..3f0e338f4 100644 --- a/pkgs/swiftgen/pubspec.yaml +++ b/pkgs/swiftgen/pubspec.yaml @@ -19,6 +19,7 @@ environment: dependencies: ffi: ^2.1.0 + meta: ^1.16.0 dev_dependencies: dart_flutter_team_lints: ^2.0.0